From 5e8b5e09e504e2780e02526acba792b340a571a4 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 24 Feb 2023 12:27:37 -0500 Subject: [PATCH 01/90] Avoid destructively modifying result.data in QueryInfo#markResult. Should help with #9293. --- src/core/QueryInfo.ts | 31 ++++++++++++++++++++----------- src/core/QueryManager.ts | 4 ++-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index 4ddb60fce34..44540c281b7 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -361,7 +361,7 @@ export class QueryInfo { "variables" | "fetchPolicy" | "errorPolicy" >, cacheWriteBehavior: CacheWriteBehavior - ) { + ): typeof result { const merger = new DeepMerger(); const graphQLErrors = isNonEmptyArray(result.errors) ? result.errors.slice(0) @@ -408,7 +408,10 @@ export class QueryInfo { }); this.lastWrite = { - result, + // Make a shallow defensive copy of the result object, in case we + // later later modify result.data in place, since we don't want + // that mutation affecting the saved lastWrite.result.data. + result: { ...result }, variables: options.variables, dmCount: destructiveMethodCounts.get(this.cache), }; @@ -448,7 +451,10 @@ export class QueryInfo { if (this.lastDiff && this.lastDiff.diff.complete) { // Reuse data from the last good (complete) diff that we // received, when possible. - result.data = this.lastDiff.diff.result; + result = { + ...result, + data: this.lastDiff.diff.result, + }; return; } // If the previous this.diff was incomplete, fall through to @@ -470,20 +476,23 @@ export class QueryInfo { this.updateWatch(options.variables); } - // If we're allowed to write to the cache, and we can read a - // complete result from the cache, update result.data to be the - // result from the cache, rather than the raw network result. - // Set without setDiff to avoid triggering a notify call, since - // we have other ways of notifying for this result. + // If we're allowed to write to the cache, update result.data to be + // the result as re-read from the cache, rather than the raw network + // result. Set without setDiff to avoid triggering a notify call, + // since we have other ways of notifying for this result. this.updateLastDiff(diff, diffOptions); - if (diff.complete) { - result.data = diff.result; - } + result = { + ...result, + // TODO Improve types so we don't need this cast. + data: diff.result as any, + }; }); } else { this.lastWrite = void 0; } } + + return result; } public markReady() { diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index efdd3c05a92..799fcba9220 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -1176,11 +1176,11 @@ export class QueryManager { // Use linkDocument rather than queryInfo.document so the // operation/fragments used to write the result are the same as the // ones used to obtain it from the link. - queryInfo.markResult( + result = queryInfo.markResult( result, linkDocument, options, - cacheWriteBehavior + cacheWriteBehavior, ); queryInfo.markReady(); } From 7f9c5ac620112d2f7cab5d419bf8b414d74ca844 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Tue, 29 Aug 2023 15:19:27 -0400 Subject: [PATCH 02/90] Test adjustments after fixing QueryInfo#markResult. Most of these test tweaks are reasonable improvements necessitated by fixing a bug that allowed queries to receive raw network results with extraneous fields when the results were incomplete. Now, the extraneous fields are no longer delivered, since they were not requested. The test I removed completely does not make sense, and was only passing previously because of the mock link running out of results. --- src/__tests__/client.ts | 7 +- src/core/__tests__/QueryManager/index.ts | 113 +------------------- src/react/hooks/__tests__/useQuery.test.tsx | 7 +- 3 files changed, 13 insertions(+), 114 deletions(-) diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index c89098362d4..4a3d7426f00 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -2921,7 +2921,12 @@ describe("client", () => { return client .query({ query }) .then(({ data }) => { - expect(data).toEqual(result.data); + const { price, ...todoWithoutPrice } = data.todos[0]; + expect(data).toEqual({ + todos: [ + todoWithoutPrice, + ], + }); }) .then(resolve, reject); } diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index 77fd0fe6dbd..e1eeb02893c 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -66,14 +66,6 @@ export function resetStore(qm: QueryManager) { } describe("QueryManager", () => { - // Standard "get id from object" method. - const dataIdFromObject = (object: any) => { - if (object.__typename && object.id) { - return object.__typename + "__" + object.id; - } - return undefined; - }; - // Helper method that serves as the constructor method for // QueryManager but has defaults that make sense for these // tests. @@ -2224,107 +2216,6 @@ describe("QueryManager", () => { } ); - itAsync( - "should not return stale data when we orphan a real-id node in the store with a real-id node", - (resolve, reject) => { - const query1 = gql` - query { - author { - name { - firstName - lastName - } - age - id - __typename - } - } - `; - const query2 = gql` - query { - author { - name { - firstName - } - id - __typename - } - } - `; - const data1 = { - author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: 18, - id: "187", - __typename: "Author", - }, - }; - const data2 = { - author: { - name: { - firstName: "John", - }, - id: "197", - __typename: "Author", - }, - }; - const reducerConfig = { dataIdFromObject }; - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query: query1 }, - result: { data: data1 }, - } - ).setOnError(reject), - config: reducerConfig, - }); - - const observable1 = queryManager.watchQuery({ query: query1 }); - const observable2 = queryManager.watchQuery({ query: query2 }); - - // I'm not sure the waiting 60 here really is required, but the test used to do it - return Promise.all([ - observableToPromise( - { - observable: observable1, - wait: 60, - }, - (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - observableToPromise( - { - observable: observable2, - wait: 60, - }, - (result) => { - expect(result).toEqual({ - data: data2, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - ]).then(resolve, reject); - } - ); - itAsync( "should return partial data when configured when we orphan a real-id node in the store with a real-id node", (resolve, reject) => { @@ -2519,9 +2410,7 @@ describe("QueryManager", () => { loading: false, networkStatus: NetworkStatus.ready, data: { - info: { - a: "ay", - }, + info: {}, }, }); setTimeout(resolve, 100); diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 96810b6414e..6846523e695 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -5706,7 +5706,12 @@ describe("useQuery Hook", () => { }, { interval: 1 } ); - expect(result.current.data).toEqual(carData); + const { vine, ...carDataWithoutVine } = carData.cars[0]; + expect(result.current.data).toEqual({ + cars: [ + carDataWithoutVine, + ], + }); expect(result.current.error).toBeUndefined(); expect(errorSpy).toHaveBeenCalled(); From a2011110a7f0c90286de15ce788f5fb4226c0c18 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Tue, 29 Aug 2023 17:09:11 -0400 Subject: [PATCH 03/90] New test of re-reading incomplete network results from cache. --- src/cache/inmemory/__tests__/client.ts | 156 +++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/cache/inmemory/__tests__/client.ts diff --git a/src/cache/inmemory/__tests__/client.ts b/src/cache/inmemory/__tests__/client.ts new file mode 100644 index 00000000000..dd85d7c331e --- /dev/null +++ b/src/cache/inmemory/__tests__/client.ts @@ -0,0 +1,156 @@ +// This file contains InMemoryCache-specific tests that exercise the +// ApolloClient class. Other test modules in this directory only test +// InMemoryCache and related utilities, without involving ApolloClient. + +import { ApolloClient, WatchQueryFetchPolicy, gql } from "../../../core"; +import { ApolloLink } from "../../../link/core"; +import { Observable } from "../../../utilities"; +import { InMemoryCache } from "../.."; +import { subscribeAndCount } from "../../../testing"; + +describe("InMemoryCache tests exercising ApolloClient", () => { + it.each([ + "cache-first", + "network-only", + "cache-and-network", + "cache-only", + "no-cache", + ])("results should be read from cache even when incomplete (fetchPolicy %s)", fetchPolicy => { + const dateString = new Date().toISOString(); + + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + date: { + read(existing) { + return new Date(existing || dateString); + }, + }, + }, + }, + }, + }); + + const client = new ApolloClient({ + link: new ApolloLink(operation => new Observable(observer => { + observer.next({ + data: { + // This raw string should be converted to a Date by the Query.date + // read function passed to InMemoryCache below. + date: dateString, + // Make sure we don't accidentally return fields not mentioned in + // the query just because the result is incomplete. + ignored: "irrelevant to the subscribed query", + // Note the Query.missing field is, well, missing. + }, + }); + setTimeout(() => { + observer.complete(); + }, 10); + })), + cache, + }); + + const query = gql` + query { + date + missing + } + `; + + const observable = client.watchQuery({ + query, + fetchPolicy, // Varies with each test iteration + returnPartialData: true, + }); + + return new Promise((resolve, reject) => { + subscribeAndCount(reject, observable, (handleCount, result) => { + let adjustedCount = handleCount; + if ( + fetchPolicy === "network-only" || + fetchPolicy === "no-cache" || + fetchPolicy === "cache-only" + ) { + // The network-only, no-cache, and cache-only fetch policies do not + // deliver a loading:true result initially, so we adjust the + // handleCount to skip that case. + ++adjustedCount; + } + + // The only fetch policy that does not re-read results from the cache is + // the "no-cache" policy. In this test, that means the Query.date field + // will remain as a raw string rather than being converted to a Date by + // the read function. + const expectedDate = + fetchPolicy === "no-cache" ? dateString : new Date(dateString); + + if (adjustedCount === 1) { + expect(result.loading).toBe(true); + expect(result.data).toEqual({ + date: expectedDate, + }); + + } else if (adjustedCount === 2) { + expect(result.loading).toBe(false); + expect(result.data).toEqual({ + date: expectedDate, + // The no-cache fetch policy does return extraneous fields from the + // raw network result that were not requested in the query, since + // the cache is not consulted. + ...(fetchPolicy === "no-cache" ? { + ignored: "irrelevant to the subscribed query" + } : null), + }); + + if (fetchPolicy === "no-cache") { + // The "no-cache" fetch policy does not receive updates from the + // cache, so we finish the test early (passing). + setTimeout(() => resolve(), 20); + + } else { + cache.writeQuery({ + query: gql`query { missing }`, + data: { + missing: "not missing anymore", + }, + }); + } + + } else if (adjustedCount === 3) { + expect(result.loading).toBe(false); + expect(result.data).toEqual({ + date: expectedDate, + missing: "not missing anymore", + }); + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + // The cache-only fetch policy does not receive updates from the + // network, so it never ends up writing the date field into the + // cache explicitly, though Query.date can still be synthesized by + // the read function. + ...(fetchPolicy === "cache-only" ? null : { + // Make sure this field is stored internally as a raw string. + date: dateString, + }), + // Written explicitly with cache.writeQuery above. + missing: "not missing anymore", + // The ignored field is never written to the cache, because it is + // not included in the query. + }, + }); + + // Wait 20ms to give the test a chance to fail if there are unexpected + // additional results. + setTimeout(() => resolve(), 20); + + } else { + reject(new Error(`Unexpected count ${adjustedCount}`)); + } + }); + }); + }); +}); From 7c2bc08b2ab46b9aa181d187a27aec2ad7129599 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 8 Sep 2023 16:53:57 -0400 Subject: [PATCH 04/90] Run 'npx changeset' for PR #11202. --- .changeset/shaggy-ears-scream.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/shaggy-ears-scream.md diff --git a/.changeset/shaggy-ears-scream.md b/.changeset/shaggy-ears-scream.md new file mode 100644 index 00000000000..3ec33bfab58 --- /dev/null +++ b/.changeset/shaggy-ears-scream.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Prevent `QueryInfo#markResult` mutation of `result.data` and return cache data consistently whether complete or incomplete. From a031860ae85b85d5aafb945c4cf1ae61844262b1 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 13 Sep 2023 16:48:47 -0400 Subject: [PATCH 05/90] Run 'npm run format' for code modified in PR #11202. --- src/__tests__/client.ts | 4 +- src/cache/inmemory/__tests__/client.ts | 252 ++++++++++---------- src/core/QueryManager.ts | 2 +- src/react/hooks/__tests__/useQuery.test.tsx | 4 +- 4 files changed, 134 insertions(+), 128 deletions(-) diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index 4a3d7426f00..9df645767c7 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -2923,9 +2923,7 @@ describe("client", () => { .then(({ data }) => { const { price, ...todoWithoutPrice } = data.todos[0]; expect(data).toEqual({ - todos: [ - todoWithoutPrice, - ], + todos: [todoWithoutPrice], }); }) .then(resolve, reject); diff --git a/src/cache/inmemory/__tests__/client.ts b/src/cache/inmemory/__tests__/client.ts index dd85d7c331e..86c4e8ee077 100644 --- a/src/cache/inmemory/__tests__/client.ts +++ b/src/cache/inmemory/__tests__/client.ts @@ -15,142 +15,152 @@ describe("InMemoryCache tests exercising ApolloClient", () => { "cache-and-network", "cache-only", "no-cache", - ])("results should be read from cache even when incomplete (fetchPolicy %s)", fetchPolicy => { - const dateString = new Date().toISOString(); - - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - date: { - read(existing) { - return new Date(existing || dateString); + ])( + "results should be read from cache even when incomplete (fetchPolicy %s)", + (fetchPolicy) => { + const dateString = new Date().toISOString(); + + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + date: { + read(existing) { + return new Date(existing || dateString); + }, }, }, }, }, - }, - }); - - const client = new ApolloClient({ - link: new ApolloLink(operation => new Observable(observer => { - observer.next({ - data: { - // This raw string should be converted to a Date by the Query.date - // read function passed to InMemoryCache below. - date: dateString, - // Make sure we don't accidentally return fields not mentioned in - // the query just because the result is incomplete. - ignored: "irrelevant to the subscribed query", - // Note the Query.missing field is, well, missing. - }, - }); - setTimeout(() => { - observer.complete(); - }, 10); - })), - cache, - }); - - const query = gql` - query { - date - missing - } - `; + }); - const observable = client.watchQuery({ - query, - fetchPolicy, // Varies with each test iteration - returnPartialData: true, - }); + const client = new ApolloClient({ + link: new ApolloLink( + (operation) => + new Observable((observer) => { + observer.next({ + data: { + // This raw string should be converted to a Date by the Query.date + // read function passed to InMemoryCache below. + date: dateString, + // Make sure we don't accidentally return fields not mentioned in + // the query just because the result is incomplete. + ignored: "irrelevant to the subscribed query", + // Note the Query.missing field is, well, missing. + }, + }); + setTimeout(() => { + observer.complete(); + }, 10); + }) + ), + cache, + }); - return new Promise((resolve, reject) => { - subscribeAndCount(reject, observable, (handleCount, result) => { - let adjustedCount = handleCount; - if ( - fetchPolicy === "network-only" || - fetchPolicy === "no-cache" || - fetchPolicy === "cache-only" - ) { - // The network-only, no-cache, and cache-only fetch policies do not - // deliver a loading:true result initially, so we adjust the - // handleCount to skip that case. - ++adjustedCount; + const query = gql` + query { + date + missing } + `; - // The only fetch policy that does not re-read results from the cache is - // the "no-cache" policy. In this test, that means the Query.date field - // will remain as a raw string rather than being converted to a Date by - // the read function. - const expectedDate = - fetchPolicy === "no-cache" ? dateString : new Date(dateString); + const observable = client.watchQuery({ + query, + fetchPolicy, // Varies with each test iteration + returnPartialData: true, + }); - if (adjustedCount === 1) { - expect(result.loading).toBe(true); - expect(result.data).toEqual({ - date: expectedDate, - }); + return new Promise((resolve, reject) => { + subscribeAndCount(reject, observable, (handleCount, result) => { + let adjustedCount = handleCount; + if ( + fetchPolicy === "network-only" || + fetchPolicy === "no-cache" || + fetchPolicy === "cache-only" + ) { + // The network-only, no-cache, and cache-only fetch policies do not + // deliver a loading:true result initially, so we adjust the + // handleCount to skip that case. + ++adjustedCount; + } - } else if (adjustedCount === 2) { - expect(result.loading).toBe(false); - expect(result.data).toEqual({ - date: expectedDate, - // The no-cache fetch policy does return extraneous fields from the - // raw network result that were not requested in the query, since - // the cache is not consulted. - ...(fetchPolicy === "no-cache" ? { - ignored: "irrelevant to the subscribed query" - } : null), - }); + // The only fetch policy that does not re-read results from the cache is + // the "no-cache" policy. In this test, that means the Query.date field + // will remain as a raw string rather than being converted to a Date by + // the read function. + const expectedDate = + fetchPolicy === "no-cache" ? dateString : new Date(dateString); + + if (adjustedCount === 1) { + expect(result.loading).toBe(true); + expect(result.data).toEqual({ + date: expectedDate, + }); + } else if (adjustedCount === 2) { + expect(result.loading).toBe(false); + expect(result.data).toEqual({ + date: expectedDate, + // The no-cache fetch policy does return extraneous fields from the + // raw network result that were not requested in the query, since + // the cache is not consulted. + ...(fetchPolicy === "no-cache" + ? { + ignored: "irrelevant to the subscribed query", + } + : null), + }); - if (fetchPolicy === "no-cache") { - // The "no-cache" fetch policy does not receive updates from the - // cache, so we finish the test early (passing). - setTimeout(() => resolve(), 20); + if (fetchPolicy === "no-cache") { + // The "no-cache" fetch policy does not receive updates from the + // cache, so we finish the test early (passing). + setTimeout(() => resolve(), 20); + } else { + cache.writeQuery({ + query: gql` + query { + missing + } + `, + data: { + missing: "not missing anymore", + }, + }); + } + } else if (adjustedCount === 3) { + expect(result.loading).toBe(false); + expect(result.data).toEqual({ + date: expectedDate, + missing: "not missing anymore", + }); - } else { - cache.writeQuery({ - query: gql`query { missing }`, - data: { + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + // The cache-only fetch policy does not receive updates from the + // network, so it never ends up writing the date field into the + // cache explicitly, though Query.date can still be synthesized by + // the read function. + ...(fetchPolicy === "cache-only" + ? null + : { + // Make sure this field is stored internally as a raw string. + date: dateString, + }), + // Written explicitly with cache.writeQuery above. missing: "not missing anymore", + // The ignored field is never written to the cache, because it is + // not included in the query. }, }); - } - - } else if (adjustedCount === 3) { - expect(result.loading).toBe(false); - expect(result.data).toEqual({ - date: expectedDate, - missing: "not missing anymore", - }); - - expect(cache.extract()).toEqual({ - ROOT_QUERY: { - __typename: "Query", - // The cache-only fetch policy does not receive updates from the - // network, so it never ends up writing the date field into the - // cache explicitly, though Query.date can still be synthesized by - // the read function. - ...(fetchPolicy === "cache-only" ? null : { - // Make sure this field is stored internally as a raw string. - date: dateString, - }), - // Written explicitly with cache.writeQuery above. - missing: "not missing anymore", - // The ignored field is never written to the cache, because it is - // not included in the query. - }, - }); - // Wait 20ms to give the test a chance to fail if there are unexpected - // additional results. - setTimeout(() => resolve(), 20); - - } else { - reject(new Error(`Unexpected count ${adjustedCount}`)); - } + // Wait 20ms to give the test a chance to fail if there are unexpected + // additional results. + setTimeout(() => resolve(), 20); + } else { + reject(new Error(`Unexpected count ${adjustedCount}`)); + } + }); }); - }); - }); + } + ); }); diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 799fcba9220..15f81132194 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -1180,7 +1180,7 @@ export class QueryManager { result, linkDocument, options, - cacheWriteBehavior, + cacheWriteBehavior ); queryInfo.markReady(); } diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 6846523e695..3cd7a0158e7 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -5708,9 +5708,7 @@ describe("useQuery Hook", () => { ); const { vine, ...carDataWithoutVine } = carData.cars[0]; expect(result.current.data).toEqual({ - cars: [ - carDataWithoutVine, - ], + cars: [carDataWithoutVine], }); expect(result.current.error).toBeUndefined(); From d8c6072e255529a42448e26d00e8d849e4e1c219 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Wed, 13 Sep 2023 16:51:34 -0400 Subject: [PATCH 06/90] Bump size-limit +8 bytes. --- .size-limit.cjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.cjs b/.size-limit.cjs index 096d4ebcf85..064303a8ea8 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -10,7 +10,7 @@ const checks = [ { path: "dist/index.js", import: "{ ApolloClient, InMemoryCache, HttpLink }", - limit: "32044", + limit: "32052", }, ...[ "ApolloProvider", From 2ad84850ca5c812ca9d0bbb818190389b2bd43d2 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 14 Sep 2023 15:24:35 +0200 Subject: [PATCH 07/90] extend test to distinguish between cache & network --- src/cache/inmemory/__tests__/client.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/cache/inmemory/__tests__/client.ts b/src/cache/inmemory/__tests__/client.ts index 86c4e8ee077..c3844cb20c0 100644 --- a/src/cache/inmemory/__tests__/client.ts +++ b/src/cache/inmemory/__tests__/client.ts @@ -18,7 +18,8 @@ describe("InMemoryCache tests exercising ApolloClient", () => { ])( "results should be read from cache even when incomplete (fetchPolicy %s)", (fetchPolicy) => { - const dateString = new Date().toISOString(); + const dateFromCache = "2023-09-14T13:03:22.616Z"; + const dateFromNetwork = "2023-09-15T13:03:22.616Z"; const cache = new InMemoryCache({ typePolicies: { @@ -26,7 +27,7 @@ describe("InMemoryCache tests exercising ApolloClient", () => { fields: { date: { read(existing) { - return new Date(existing || dateString); + return new Date(existing || dateFromCache); }, }, }, @@ -42,7 +43,7 @@ describe("InMemoryCache tests exercising ApolloClient", () => { data: { // This raw string should be converted to a Date by the Query.date // read function passed to InMemoryCache below. - date: dateString, + date: dateFromNetwork, // Make sure we don't accidentally return fields not mentioned in // the query just because the result is incomplete. ignored: "irrelevant to the subscribed query", @@ -88,18 +89,22 @@ describe("InMemoryCache tests exercising ApolloClient", () => { // the "no-cache" policy. In this test, that means the Query.date field // will remain as a raw string rather than being converted to a Date by // the read function. - const expectedDate = - fetchPolicy === "no-cache" ? dateString : new Date(dateString); + const expectedDateAfterResult = + fetchPolicy === "cache-only" + ? new Date(dateFromCache) + : fetchPolicy === "no-cache" + ? dateFromNetwork + : new Date(dateFromNetwork); if (adjustedCount === 1) { expect(result.loading).toBe(true); expect(result.data).toEqual({ - date: expectedDate, + date: new Date(dateFromCache), }); } else if (adjustedCount === 2) { expect(result.loading).toBe(false); expect(result.data).toEqual({ - date: expectedDate, + date: expectedDateAfterResult, // The no-cache fetch policy does return extraneous fields from the // raw network result that were not requested in the query, since // the cache is not consulted. @@ -129,7 +134,7 @@ describe("InMemoryCache tests exercising ApolloClient", () => { } else if (adjustedCount === 3) { expect(result.loading).toBe(false); expect(result.data).toEqual({ - date: expectedDate, + date: expectedDateAfterResult, missing: "not missing anymore", }); @@ -144,7 +149,7 @@ describe("InMemoryCache tests exercising ApolloClient", () => { ? null : { // Make sure this field is stored internally as a raw string. - date: dateString, + date: dateFromNetwork, }), // Written explicitly with cache.writeQuery above. missing: "not missing anymore", From ec5d86c2efb676870da4e1857c1fc578fa32958c Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 14 Sep 2023 18:12:24 +0200 Subject: [PATCH 08/90] defensive copy of `result` at the top of `markResult` --- src/core/QueryInfo.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index 44540c281b7..f666456160c 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -362,6 +362,7 @@ export class QueryInfo { >, cacheWriteBehavior: CacheWriteBehavior ): typeof result { + result = { ...result }; const merger = new DeepMerger(); const graphQLErrors = isNonEmptyArray(result.errors) ? result.errors.slice(0) @@ -451,10 +452,7 @@ export class QueryInfo { if (this.lastDiff && this.lastDiff.diff.complete) { // Reuse data from the last good (complete) diff that we // received, when possible. - result = { - ...result, - data: this.lastDiff.diff.result, - }; + result.data = this.lastDiff.diff.result; return; } // If the previous this.diff was incomplete, fall through to @@ -481,11 +479,7 @@ export class QueryInfo { // result. Set without setDiff to avoid triggering a notify call, // since we have other ways of notifying for this result. this.updateLastDiff(diff, diffOptions); - result = { - ...result, - // TODO Improve types so we don't need this cast. - data: diff.result as any, - }; + result.data = diff.result; }); } else { this.lastWrite = void 0; From 9ee47f839390174d228920912711b6e5487b66a3 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Fri, 15 Sep 2023 11:21:07 -0400 Subject: [PATCH 09/90] Add `.changeset/` directory to `.prettierignore`. Since this directory's contents are managed by the `changeset` tool, it seems like running the files through Prettier will only lead to formatting churn, as seen here: https://app.circleci.com/pipelines/github/apollographql/apollo-client/21435/workflows/0d888903-9898-4c83-bab2-7146b7c3897e/jobs/111306 --- .prettierignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.prettierignore b/.prettierignore index 5c9398e78de..156d707c16e 100644 --- a/.prettierignore +++ b/.prettierignore @@ -28,3 +28,4 @@ node_modules/ .yalc/ .next/ +.changeset/ From c1b8c9133701aaf5053d1d14729b70a4d63ed8d8 Mon Sep 17 00:00:00 2001 From: Alessia Bellisario Date: Tue, 19 Sep 2023 06:11:44 -0700 Subject: [PATCH 10/90] chore: enter prerelease mode --- .changeset/pre.json | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 00000000000..6c10751aa29 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,10 @@ +{ + "mode": "pre", + "tag": "alpha", + "initialVersions": { + "@apollo/client": "3.8.3" + }, + "changesets": [ + "shaggy-ears-scream.md" + ] +} From 8d2b4e107d7c21563894ced3a65d631183b58fd9 Mon Sep 17 00:00:00 2001 From: prowe Date: Tue, 19 Sep 2023 08:40:24 -0500 Subject: [PATCH 11/90] Ability to dynamically match mocks (#6701) --- .api-reports/api-report-core.md | 2 +- .api-reports/api-report-react.md | 2 +- .api-reports/api-report-react_components.md | 2 +- .api-reports/api-report-react_context.md | 2 +- .api-reports/api-report-react_hoc.md | 2 +- .api-reports/api-report-react_hooks.md | 2 +- .api-reports/api-report-react_ssr.md | 2 +- .api-reports/api-report-testing.md | 13 +- .api-reports/api-report-testing_core.md | 13 +- .api-reports/api-report-utilities.md | 2 +- .api-reports/api-report.md | 2 +- .changeset/sour-sheep-walk.md | 7 + docs/source/development-testing/testing.mdx | 42 ++++- src/testing/core/mocking/mockLink.ts | 34 +++- .../react/__tests__/MockedProvider.test.tsx | 147 +++++++++++++++++- .../MockedProvider.test.tsx.snap | 21 +++ 16 files changed, 269 insertions(+), 26 deletions(-) create mode 100644 .changeset/sour-sheep-walk.md diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 0bc6fab994b..e3138c81d29 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -1642,7 +1642,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 504453b9637..1bb039c96dd 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -1463,7 +1463,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) diff --git a/.api-reports/api-report-react_components.md b/.api-reports/api-report-react_components.md index cf07c01004a..71852806729 100644 --- a/.api-reports/api-report-react_components.md +++ b/.api-reports/api-report-react_components.md @@ -1265,7 +1265,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) diff --git a/.api-reports/api-report-react_context.md b/.api-reports/api-report-react_context.md index fe5b3e684cb..3bdc1ed79b5 100644 --- a/.api-reports/api-report-react_context.md +++ b/.api-reports/api-report-react_context.md @@ -1175,7 +1175,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) diff --git a/.api-reports/api-report-react_hoc.md b/.api-reports/api-report-react_hoc.md index bcfbf8c55c5..6fc66b904e1 100644 --- a/.api-reports/api-report-react_hoc.md +++ b/.api-reports/api-report-react_hoc.md @@ -1243,7 +1243,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index 71597296826..052cfd6c223 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -1393,7 +1393,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) diff --git a/.api-reports/api-report-react_ssr.md b/.api-reports/api-report-react_ssr.md index 4e617cff33e..95a9ed6957f 100644 --- a/.api-reports/api-report-react_ssr.md +++ b/.api-reports/api-report-react_ssr.md @@ -1162,7 +1162,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index 70b9d593258..fdff4903c39 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -892,7 +892,11 @@ export interface MockedResponse, TVariables = Record // (undocumented) request: GraphQLRequest; // (undocumented) - result?: FetchResult | ResultFunction>; + result?: FetchResult | ResultFunction, TVariables>; + // Warning: (ae-forgotten-export) The symbol "VariableMatcher" needs to be exported by the entry point index.d.ts + // + // (undocumented) + variableMatcher?: VariableMatcher; } // @public (undocumented) @@ -1238,7 +1242,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1497,7 +1501,7 @@ interface Resolvers { } // @public (undocumented) -export type ResultFunction = () => T; +export type ResultFunction> = (variables: V) => T; // @public (undocumented) type SafeReadonly = T extends object ? Readonly : T; @@ -1623,6 +1627,9 @@ interface UriFunction { (operation: Operation): string; } +// @public (undocumented) +type VariableMatcher> = (variables: V) => boolean; + // @public (undocumented) export function wait(ms: number): Promise; diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 2dc55496b8d..3cf490dd0d3 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -848,7 +848,11 @@ export interface MockedResponse, TVariables = Record // (undocumented) request: GraphQLRequest; // (undocumented) - result?: FetchResult | ResultFunction>; + result?: FetchResult | ResultFunction, TVariables>; + // Warning: (ae-forgotten-export) The symbol "VariableMatcher" needs to be exported by the entry point index.d.ts + // + // (undocumented) + variableMatcher?: VariableMatcher; } // @public (undocumented) @@ -1194,7 +1198,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1455,7 +1459,7 @@ interface Resolvers { } // @public (undocumented) -export type ResultFunction = () => T; +export type ResultFunction> = (variables: V) => T; // @public (undocumented) type SafeReadonly = T extends object ? Readonly : T; @@ -1581,6 +1585,9 @@ interface UriFunction { (operation: Operation): string; } +// @public (undocumented) +type VariableMatcher> = (variables: V) => boolean; + // @public (undocumented) export function wait(ms: number): Promise; diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 410b9ded70b..cc97fb9dbe6 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -1905,7 +1905,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 01950a8f066..8c933201f08 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -2017,7 +2017,7 @@ class QueryInfo { // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts // // (undocumented) - markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) diff --git a/.changeset/sour-sheep-walk.md b/.changeset/sour-sheep-walk.md new file mode 100644 index 00000000000..b0270d5ee68 --- /dev/null +++ b/.changeset/sour-sheep-walk.md @@ -0,0 +1,7 @@ +--- +"@apollo/client": minor +--- + +Ability to dynamically match mocks + +Adds support for a new property `MockedResponse.variableMatcher`: a predicate function that accepts a `variables` param. If `true`, the `variables` will be passed into the `ResultFunction` to help dynamically build a response. diff --git a/docs/source/development-testing/testing.mdx b/docs/source/development-testing/testing.mdx index 06a69d5db25..7fcb53662d3 100644 --- a/docs/source/development-testing/testing.mdx +++ b/docs/source/development-testing/testing.mdx @@ -101,7 +101,7 @@ Each mock object defines a `request` field (indicating the shape and variables o Alternatively, the `result` field can be a function that returns a mocked response after performing arbitrary logic: ```jsx -result: () => { +result: (variables) => { // `variables` is optional // ...arbitrary logic... return { @@ -150,6 +150,46 @@ it("renders without error", async () => { +### Dynamic variables + +Sometimes, the exact value of the variables being passed are not known. The `MockedResponse` object takes a `variableMatcher` property that is a function that takes the variables and returns a boolean indication if this mock should match the invocation for the provided query. You cannot specify this parameter and `request.variables` at the same time. + +For example, this mock will match all dog queries: + +```ts +import { MockedResponse } from "@apollo/client/testing"; + +const dogMock: MockedResponse = { + request: { + query: GET_DOG_QUERY + }, + variableMatcher: (variables) => true, + result: { + data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } }, + }, +}; +``` + +This can also be useful for asserting specific variables individually: + +```ts +import { MockedResponse } from "@apollo/client/testing"; + +const dogMock: MockedResponse = { + request: { + query: GET_DOG_QUERY + }, + variableMatcher: jest.fn().mockReturnValue(true), + result: { + data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } }, + }, +}; + +expect(variableMatcher).toHaveBeenCalledWith(expect.objectContaining({ + name: 'Buck' +})); +``` + ### Setting `addTypename` In the example above, we set the `addTypename` prop of `MockedProvider` to `false`. This prevents Apollo Client from automatically adding the special `__typename` field to every object it queries for (it does this by default to support data normalization in the cache). diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index e02d8aaf794..bd798bd6395 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -19,16 +19,21 @@ import { print, } from "../../../utilities/index.js"; -export type ResultFunction = () => T; +export type ResultFunction> = (variables: V) => T; + +export type VariableMatcher> = ( + variables: V +) => boolean; export interface MockedResponse< TData = Record, TVariables = Record, > { request: GraphQLRequest; - result?: FetchResult | ResultFunction>; + result?: FetchResult | ResultFunction, TVariables>; error?: Error; delay?: number; + variableMatcher?: VariableMatcher; newData?: ResultFunction; } @@ -93,6 +98,9 @@ export class MockLink extends ApolloLink { if (equal(requestVariables, mockedResponseVars)) { return true; } + if (res.variableMatcher && res.variableMatcher(operation.variables)) { + return true; + } unmatchedVars.push(mockedResponseVars); return false; }) @@ -131,7 +139,7 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} const { newData } = response; if (newData) { - response.result = newData(); + response.result = newData(operation.variables); mockedResponses.push(response); } @@ -165,7 +173,7 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} if (response.result) { observer.next( typeof response.result === "function" - ? (response.result as ResultFunction)() + ? response.result(operation.variables) : response.result ); } @@ -195,8 +203,26 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} if (query) { newMockedResponse.request.query = query; } + this.normalizeVariableMatching(newMockedResponse); return newMockedResponse; } + + private normalizeVariableMatching(mockedResponse: MockedResponse) { + const variables = mockedResponse.request.variables; + if (mockedResponse.variableMatcher && variables) { + throw new Error( + "Mocked response should contain either variableMatcher or request.variables" + ); + } + + if (!mockedResponse.variableMatcher) { + mockedResponse.variableMatcher = (vars) => { + const requestVariables = vars || {}; + const mockedResponseVariables = variables || {}; + return equal(requestVariables, mockedResponseVariables); + }; + } + } } export interface MockApolloLink extends ApolloLink { diff --git a/src/testing/react/__tests__/MockedProvider.test.tsx b/src/testing/react/__tests__/MockedProvider.test.tsx index 8dd2b3be043..e3c8a660c16 100644 --- a/src/testing/react/__tests__/MockedProvider.test.tsx +++ b/src/testing/react/__tests__/MockedProvider.test.tsx @@ -7,8 +7,8 @@ import { itAsync, MockedResponse, MockLink } from "../../core"; import { MockedProvider } from "../MockedProvider"; import { useQuery } from "../../../react/hooks"; import { InMemoryCache } from "../../../cache"; -import { ApolloLink } from "../../../link/core"; -import { spyOnConsole } from "../../internal"; +import { ApolloLink, FetchResult } from "../../../link/core"; +import { Observable } from "zen-observable-ts"; const variables = { username: "mock_username", @@ -62,7 +62,7 @@ interface Variables { let errorThrown = false; const errorLink = new ApolloLink((operation, forward) => { - let observer = null; + let observer: Observable | null = null; try { observer = forward(operation); } catch (error) { @@ -98,6 +98,100 @@ describe("General use", () => { }).then(resolve, reject); }); + itAsync( + "should pass the variables to the result function", + async (resolve, reject) => { + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; + } + + const mock2: MockedResponse = { + request: { + query, + variables, + }, + result: jest.fn().mockResolvedValue({ data: { user } }), + }; + + render( + + + + ); + + waitFor(() => { + expect(mock2.result as jest.Mock).toHaveBeenCalledWith(variables); + }).then(resolve, reject); + } + ); + + itAsync( + "should pass the variables to the variableMatcher", + async (resolve, reject) => { + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; + } + + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: jest.fn().mockReturnValue(true), + result: { data: { user } }, + }; + + render( + + + + ); + + waitFor(() => { + expect(mock2.variableMatcher as jest.Mock).toHaveBeenCalledWith( + variables + ); + }).then(resolve, reject); + } + ); + + itAsync( + "should use a mock if the variableMatcher returns true", + async (resolve, reject) => { + let finished = false; + + function Component({ username }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data!.user).toMatchSnapshot(); + finished = true; + } + return null; + } + + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: (v) => v.username === variables.username, + result: { data: { user } }, + }; + + render( + + + + ); + + waitFor(() => { + expect(finished).toBe(true); + }).then(resolve, reject); + } + ); + itAsync("should allow querying with the typename", (resolve, reject) => { let finished = false; function Component({ username }: Variables) { @@ -191,6 +285,41 @@ describe("General use", () => { } ); + itAsync( + "should error if the variableMatcher returns false", + async (resolve, reject) => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; + } + return null; + } + + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: () => false, + result: { data: { user } }, + }; + + render( + + + + ); + + waitFor(() => { + expect(finished).toBe(true); + }).then(resolve, reject); + } + ); + itAsync( "should error if the variables do not deep equal", (resolve, reject) => { @@ -522,7 +651,7 @@ describe("General use", () => { }); it("shows a warning in the console when there is no matched mock", async () => { - using _consoleSpy = spyOnConsole("warn"); + const consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); let finished = false; function Component({ ...variables }: Variables) { const { loading } = useQuery(query, { variables }); @@ -562,10 +691,12 @@ describe("General use", () => { expect(console.warn).toHaveBeenCalledWith( expect.stringContaining("No more mocked responses for the query") ); + + consoleSpy.mockRestore(); }); it("silences console warning for unmatched mocks when `showWarnings` is `false`", async () => { - using _consoleSpy = spyOnConsole("warn"); + const consoleSpy = jest.spyOn(console, "warn"); let finished = false; function Component({ ...variables }: Variables) { const { loading } = useQuery(query, { variables }); @@ -602,10 +733,12 @@ describe("General use", () => { }); expect(console.warn).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); }); it("silences console warning for unmatched mocks when passing `showWarnings` to `MockLink` directly", async () => { - using _consoleSpy = spyOnConsole("warn"); + const consoleSpy = jest.spyOn(console, "warn"); let finished = false; function Component({ ...variables }: Variables) { const { loading } = useQuery(query, { variables }); @@ -646,6 +779,8 @@ describe("General use", () => { }); expect(console.warn).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); }); itAsync( diff --git a/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap b/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap index 5fecc4e98d7..727f5edbb85 100644 --- a/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap +++ b/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap @@ -18,6 +18,20 @@ Expected variables: {"username":"mock_username"} ] `; +exports[`General use should error if the variableMatcher returns false 1`] = ` +[ApolloError: No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {"username":"mock_username"} + +Failed to match 1 mock for this query. The mocked response had the following variables: + {} +] +`; + exports[`General use should error if the variables do not deep equal 1`] = ` [ApolloError: No more mocked responses for the query: query GetUser($username: String!) { user(username: $username) { @@ -87,3 +101,10 @@ exports[`General use should support custom error handling using setOnError 1`] = Expected variables: {"username":"mock_username"} ] `; + +exports[`General use should use a mock if the variableMatcher returns true 1`] = ` +Object { + "__typename": "User", + "id": "user_id", +} +`; From a0fbcd8f643c93aae9dd0a05c71ec41ac7b56e52 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 19 Sep 2023 07:54:35 -0700 Subject: [PATCH 12/90] Version Packages (alpha) (#11210) * Version Packages (alpha) * chore: remove unreleased changeset from main --------- Co-authored-by: github-actions[bot] Co-authored-by: Alessia Bellisario --- .changeset/pre.json | 3 ++- CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 6c10751aa29..224d110b20f 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -5,6 +5,7 @@ "@apollo/client": "3.8.3" }, "changesets": [ - "shaggy-ears-scream.md" + "shaggy-ears-scream", + "sour-sheep-walk" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ec64318480..16b294000bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # @apollo/client +## 3.9.0-alpha.0 + +### Minor Changes + +- [#11202](https://github.com/apollographql/apollo-client/pull/11202) [`7c2bc08b2`](https://github.com/apollographql/apollo-client/commit/7c2bc08b2ab46b9aa181d187a27aec2ad7129599) Thanks [@benjamn](https://github.com/benjamn)! - Prevent `QueryInfo#markResult` mutation of `result.data` and return cache data consistently whether complete or incomplete. + +- [#6701](https://github.com/apollographql/apollo-client/pull/6701) [`8d2b4e107`](https://github.com/apollographql/apollo-client/commit/8d2b4e107d7c21563894ced3a65d631183b58fd9) Thanks [@prowe](https://github.com/prowe)! - Ability to dynamically match mocks + + Adds support for a new property `MockedResponse.variableMatcher`: a predicate function that accepts a `variables` param. If `true`, the `variables` will be passed into the `ResultFunction` to help dynamically build a response. + ## 3.8.3 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index cd0e1e2c3ee..11635389387 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.8.3", + "version": "3.9.0-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.8.3", + "version": "3.9.0-alpha.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 46e6b576d72..539503849bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.8.3", + "version": "3.9.0-alpha.0", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 4cdcb9142efcaeec715ac86c292cc49e2654bd63 Mon Sep 17 00:00:00 2001 From: Alessia Bellisario Date: Tue, 19 Sep 2023 10:35:02 -0700 Subject: [PATCH 13/90] Version Packages From 217c59ec3222548df521a57b3002c6f973c30808 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 20 Sep 2023 17:02:29 +0200 Subject: [PATCH 14/90] remove peerDependency on graphql 14 (#11231) --- .circleci/config.yml | 32 ++++++++++++++++++++ integration-tests/peerdeps-tsc/.gitignore | 5 +++ integration-tests/peerdeps-tsc/package.json | 20 ++++++++++++ integration-tests/peerdeps-tsc/src/index.ts | 1 + integration-tests/peerdeps-tsc/tsconfig.json | 17 +++++++++++ package.json | 2 +- 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 integration-tests/peerdeps-tsc/.gitignore create mode 100644 integration-tests/peerdeps-tsc/package.json create mode 100644 integration-tests/peerdeps-tsc/src/index.ts create mode 100644 integration-tests/peerdeps-tsc/tsconfig.json diff --git a/.circleci/config.yml b/.circleci/config.yml index 4743a55f95e..65d5c98dda7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -86,6 +86,24 @@ jobs: command: npm run test --workspace=<< parameters.framework >> working_directory: integration-tests + TestPeerDepTypes: + parameters: + externalPackage: + type: string + docker: + - image: cimg/node:20.6.1 + steps: + - checkout + - attach_workspace: + at: /tmp/workspace + - run: + working_directory: integration-tests/peerdeps-tsc + command: | + npm install + npm install @apollo/client@/tmp/workspace/apollo-client.tgz + npm install << parameters.externalPackage >> + npm test + workflows: Build and Test: jobs: @@ -109,3 +127,17 @@ workflows: - vite - vite-swc # -browser-esm would need a package publish to npm/CDNs + - TestPeerDepTypes: + name: Test external types for << matrix.externalPackage >> + requires: + - BuildTarball + matrix: + parameters: + externalPackage: + - "graphql@15" + - "graphql@16" + - "graphql@^17.0.0-alpha" + - "@types/react@16.8 @types/react-dom@16.8" + - "@types/react@17 @types/react-dom@17" + - "@types/react@18 @types/react-dom@18" + - "typescript@next" diff --git a/integration-tests/peerdeps-tsc/.gitignore b/integration-tests/peerdeps-tsc/.gitignore new file mode 100644 index 00000000000..db6846cb27a --- /dev/null +++ b/integration-tests/peerdeps-tsc/.gitignore @@ -0,0 +1,5 @@ +# explicitly avoiding to check in this one +# so we run this test always with the latest version +package-lock.json +dist +node_modules diff --git a/integration-tests/peerdeps-tsc/package.json b/integration-tests/peerdeps-tsc/package.json new file mode 100644 index 00000000000..15fa22cb969 --- /dev/null +++ b/integration-tests/peerdeps-tsc/package.json @@ -0,0 +1,20 @@ +{ + "name": "peerdeps-tsc", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "tsc" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "graphql": "^16.0.0", + "graphql-ws": "^5.5.5", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "subscriptions-transport-ws": "^0.11.0", + "typescript": "latest" + } +} diff --git a/integration-tests/peerdeps-tsc/src/index.ts b/integration-tests/peerdeps-tsc/src/index.ts new file mode 100644 index 00000000000..a0266127931 --- /dev/null +++ b/integration-tests/peerdeps-tsc/src/index.ts @@ -0,0 +1 @@ +export * from "@apollo/client"; diff --git a/integration-tests/peerdeps-tsc/tsconfig.json b/integration-tests/peerdeps-tsc/tsconfig.json new file mode 100644 index 00000000000..1dff851d2ac --- /dev/null +++ b/integration-tests/peerdeps-tsc/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "rootDir": "./src", + "outDir": "./dist", + "declaration": true, + "sourceMap": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": false, + "types": ["react", "react-dom"], + "lib": ["es2018", "dom"] + } +} diff --git a/package.json b/package.json index 0c1964f2b36..831ed6e9419 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "npm": "^7.20.3 || ^8.0.0 || ^9.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", + "graphql": "^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", From 4d64a6fa2ad5abe6f7f172c164f5e1fc2cb89829 Mon Sep 17 00:00:00 2001 From: Seba Kerckhof Date: Fri, 22 Sep 2023 00:05:54 +0200 Subject: [PATCH 15/90] Feature request 241: reuse mocks in MockLink / MockedProvider (#11178) --- .api-reports/api-report-testing.md | 2 + .api-reports/api-report-testing_core.md | 2 + .changeset/yellow-flies-repeat.md | 5 + .github/workflows/api-extractor.yml | 4 +- .size-limit.cjs | 2 +- docs/source/development-testing/testing.mdx | 31 +++ package-lock.json | 2 +- package.json | 2 +- src/testing/core/mocking/mockLink.ts | 16 +- .../react/__tests__/MockedProvider.test.tsx | 244 +++++++++++++++++- 10 files changed, 301 insertions(+), 9 deletions(-) create mode 100644 .changeset/yellow-flies-repeat.md diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index fdff4903c39..048d40b19f0 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -888,6 +888,8 @@ export interface MockedResponse, TVariables = Record // (undocumented) error?: Error; // (undocumented) + maxUsageCount?: number; + // (undocumented) newData?: ResultFunction; // (undocumented) request: GraphQLRequest; diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 3cf490dd0d3..dafb615346b 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -844,6 +844,8 @@ export interface MockedResponse, TVariables = Record // (undocumented) error?: Error; // (undocumented) + maxUsageCount?: number; + // (undocumented) newData?: ResultFunction; // (undocumented) request: GraphQLRequest; diff --git a/.changeset/yellow-flies-repeat.md b/.changeset/yellow-flies-repeat.md new file mode 100644 index 00000000000..b6fcff7db25 --- /dev/null +++ b/.changeset/yellow-flies-repeat.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Support re-using of mocks in the MockedProvider diff --git a/.github/workflows/api-extractor.yml b/.github/workflows/api-extractor.yml index 5b232f45133..e8d0f5b06d6 100644 --- a/.github/workflows/api-extractor.yml +++ b/.github/workflows/api-extractor.yml @@ -19,8 +19,6 @@ jobs: - name: Install dependencies (with cache) uses: bahmutov/npm-install@v1 - - name: Run build - run: npm run build - + # Builds the library and runs the api extractor - name: Run Api-Extractor run: npm run extract-api diff --git a/.size-limit.cjs b/.size-limit.cjs index ff6372f7591..090bc4c9dc4 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "37986", + limit: "38000", }, { path: "dist/main.cjs", diff --git a/docs/source/development-testing/testing.mdx b/docs/source/development-testing/testing.mdx index 7fcb53662d3..78402c3f021 100644 --- a/docs/source/development-testing/testing.mdx +++ b/docs/source/development-testing/testing.mdx @@ -150,6 +150,37 @@ it("renders without error", async () => { +#### Reusing mocks + +By default, a mock is only used once. If you want to reuse a mock for multiple operations, you can set the `maxUsageCount` field to a number indicating how many times the mock should be used: + + + +```jsx title="dog.test.js" +import { GET_DOG_QUERY } from "./dog"; + +const mocks = [ + { + request: { + query: GET_DOG_QUERY, + variables: { + name: "Buck" + } + }, + result: { + data: { + dog: { id: "1", name: "Buck", breed: "bulldog" } + } + }, + maxUsageCount: 2, // The mock can be used twice before it's removed, default is 1 + } +]; +``` + + + +Passing `Number.POSITIVE_INFINITY` will cause the mock to be reused indefinitely. + ### Dynamic variables Sometimes, the exact value of the variables being passed are not known. The `MockedResponse` object takes a `variableMatcher` property that is a function that takes the variables and returns a boolean indication if this mock should match the invocation for the provided query. You cannot specify this parameter and `request.variables` at the same time. diff --git a/package-lock.json b/package-lock.json index 6d8d664607d..8a5038d6d4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -103,7 +103,7 @@ "npm": "^7.20.3 || ^8.0.0 || ^9.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", + "graphql": "^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", diff --git a/package.json b/package.json index 831ed6e9419..dda56091ebd 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "prepdist": "node ./config/prepareDist.js", "prepdist:changesets": "ts-node-script config/prepareChangesetsRelease.ts", "postprocess-dist": "ts-node-script config/postprocessDist.ts", - "extract-api": "ts-node-script config/apiExtractor.ts", + "extract-api": "npm run build && ts-node-script config/apiExtractor.ts", "clean": "rimraf dist coverage lib temp", "check:format": "prettier --check .", "ci:precheck": "node config/precheck.js", diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index bd798bd6395..8b07e91065e 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -30,6 +30,7 @@ export interface MockedResponse< TVariables = Record, > { request: GraphQLRequest; + maxUsageCount?: number; result?: FetchResult | ResultFunction, TVariables>; error?: Error; delay?: number; @@ -135,8 +136,11 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} ); } } else { - mockedResponses.splice(responseIndex, 1); - + if (response.maxUsageCount! > 1) { + response.maxUsageCount!--; + } else { + mockedResponses.splice(responseIndex, 1); + } const { newData } = response; if (newData) { response.result = newData(operation.variables); @@ -203,6 +207,14 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} if (query) { newMockedResponse.request.query = query; } + + mockedResponse.maxUsageCount = mockedResponse.maxUsageCount ?? 1; + invariant( + mockedResponse.maxUsageCount > 0, + `Mock response maxUsageCount must be greater than 0, %s given`, + mockedResponse.maxUsageCount + ); + this.normalizeVariableMatching(newMockedResponse); return newMockedResponse; } diff --git a/src/testing/react/__tests__/MockedProvider.test.tsx b/src/testing/react/__tests__/MockedProvider.test.tsx index e3c8a660c16..b1676890bf7 100644 --- a/src/testing/react/__tests__/MockedProvider.test.tsx +++ b/src/testing/react/__tests__/MockedProvider.test.tsx @@ -1,12 +1,13 @@ import React from "react"; import { DocumentNode } from "graphql"; -import { render, waitFor } from "@testing-library/react"; +import { act, render, waitFor } from "@testing-library/react"; import gql from "graphql-tag"; import { itAsync, MockedResponse, MockLink } from "../../core"; import { MockedProvider } from "../MockedProvider"; import { useQuery } from "../../../react/hooks"; import { InMemoryCache } from "../../../cache"; +import { QueryResult } from "../../../react/types/types"; import { ApolloLink, FetchResult } from "../../../link/core"; import { Observable } from "zen-observable-ts"; @@ -56,6 +57,10 @@ interface Data { }; } +interface Result { + current: QueryResult | null; +} + interface Variables { username: string; } @@ -611,6 +616,243 @@ describe("General use", () => { expect(errorThrown).toBeFalsy(); }); + it("Uses a mock a configured number of times when `maxUsageCount` is configured", async () => { + const result: Result = { current: null }; + function Component({ username }: Variables) { + result.current = useQuery(query, { + variables: { username }, + }); + return null; + } + + const waitForLoaded = async () => { + await waitFor(() => { + expect(result.current?.loading).toBe(false); + expect(result.current?.error).toBeUndefined(); + }); + }; + + const waitForError = async () => { + await waitFor(() => { + expect(result.current?.error?.message).toMatch( + /No more mocked responses/ + ); + }); + }; + + const refetch = () => { + return act(async () => { + try { + await result.current?.refetch(); + } catch {} + }); + }; + + const mocks: ReadonlyArray = [ + { + request: { + query, + variables: { + username: "mock_username", + }, + }, + maxUsageCount: 2, + result: { data: { user } }, + }, + ]; + + const mockLink = new MockLink(mocks, true, { showWarnings: false }); + const link = ApolloLink.from([errorLink, mockLink]); + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + render(, { wrapper: Wrapper }); + await waitForLoaded(); + await refetch(); + await waitForLoaded(); + await refetch(); + await waitForError(); + }); + + it("Uses a mock infinite number of times when `maxUsageCount` is configured with Number.POSITIVE_INFINITY", async () => { + const result: Result = { current: null }; + function Component({ username }: Variables) { + result.current = useQuery(query, { + variables: { username }, + }); + return null; + } + + const waitForLoaded = async () => { + await waitFor(() => { + expect(result.current?.loading).toBe(false); + expect(result.current?.error).toBeUndefined(); + }); + }; + + const refetch = () => { + return act(async () => { + try { + await result.current?.refetch(); + } catch {} + }); + }; + + const mocks: ReadonlyArray = [ + { + request: { + query, + variables: { + username: "mock_username", + }, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { data: { user } }, + }, + ]; + + const mockLink = new MockLink(mocks, true, { showWarnings: false }); + const link = ApolloLink.from([errorLink, mockLink]); + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + render(, { wrapper: Wrapper }); + for (let i = 0; i < 100; i++) { + await waitForLoaded(); + await refetch(); + } + await waitForLoaded(); + }); + + it("uses a mock once when `maxUsageCount` is not configured", async () => { + const result: Result = { current: null }; + function Component({ username }: Variables) { + result.current = useQuery(query, { + variables: { username }, + }); + return null; + } + + const waitForLoaded = async () => { + await waitFor(() => { + expect(result.current?.loading).toBe(false); + expect(result.current?.error).toBeUndefined(); + }); + }; + + const waitForError = async () => { + await waitFor(() => { + expect(result.current?.error?.message).toMatch( + /No more mocked responses/ + ); + }); + }; + + const refetch = () => { + return act(async () => { + try { + await result.current?.refetch(); + } catch {} + }); + }; + + const mocks: ReadonlyArray = [ + { + request: { + query, + variables: { + username: "mock_username", + }, + }, + result: { data: { user } }, + }, + ]; + + const mockLink = new MockLink(mocks, true, { showWarnings: false }); + const link = ApolloLink.from([errorLink, mockLink]); + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + render(, { wrapper: Wrapper }); + await waitForLoaded(); + await refetch(); + await waitForError(); + }); + + it("can still use other mocks after a mock has been fully consumed", async () => { + const result: Result = { current: null }; + function Component({ username }: Variables) { + result.current = useQuery(query, { + variables: { username }, + }); + return null; + } + + const waitForLoaded = async () => { + await waitFor(() => { + expect(result.current?.loading).toBe(false); + expect(result.current?.error).toBeUndefined(); + }); + }; + + const refetch = () => { + return act(async () => { + try { + await result.current?.refetch(); + } catch {} + }); + }; + + const mocks: ReadonlyArray = [ + { + request: { + query, + variables: { + username: "mock_username", + }, + }, + maxUsageCount: 2, + result: { data: { user } }, + }, + { + request: { + query, + variables: { + username: "mock_username", + }, + }, + result: { + data: { + user: { + __typename: "User", + id: "new_id", + }, + }, + }, + }, + ]; + + const mockLink = new MockLink(mocks, true, { showWarnings: false }); + const link = ApolloLink.from([errorLink, mockLink]); + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + render(, { wrapper: Wrapper }); + await waitForLoaded(); + await refetch(); + await waitForLoaded(); + await refetch(); + await waitForLoaded(); + expect(result.current?.data?.user).toEqual({ + __typename: "User", + id: "new_id", + }); + }); + it('should return "Mocked response should contain" errors in response', async () => { let finished = false; function Component({ ...variables }: Variables) { From 33e0d787e1cc91011526dc0d9614c61f3b6b52f4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Sep 2023 15:37:06 -0700 Subject: [PATCH 16/90] Version Packages (alpha) (#11239) * Version Packages (alpha) * chore: update CHANGELOG.md --------- Co-authored-by: github-actions[bot] Co-authored-by: Alessia Bellisario --- .changeset/pre.json | 4 +++- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 224d110b20f..b16d942c18b 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -5,7 +5,9 @@ "@apollo/client": "3.8.3" }, "changesets": [ + "pretty-readers-lick", "shaggy-ears-scream", - "sour-sheep-walk" + "sour-sheep-walk", + "yellow-flies-repeat" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d3f18837d8..ad1c8113cec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/client +## 3.9.0-alpha.1 + +### Minor Changes + +- [#11178](https://github.com/apollographql/apollo-client/pull/11178) [`4d64a6fa2`](https://github.com/apollographql/apollo-client/commit/4d64a6fa2ad5abe6f7f172c164f5e1fc2cb89829) Thanks [@sebakerckhof](https://github.com/sebakerckhof)! - Support re-using of mocks in the MockedProvider + ## 3.9.0-alpha.0 ### Minor Changes diff --git a/package-lock.json b/package-lock.json index 8a5038d6d4f..8c3bf3bac69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.0", + "version": "3.9.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.0-alpha.0", + "version": "3.9.0-alpha.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index dda56091ebd..a3493152edd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.0", + "version": "3.9.0-alpha.1", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From d08970d348cf4ad6d80c6baf85b4a4cd4034a3bb Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 5 Oct 2023 04:04:26 -0400 Subject: [PATCH 17/90] Decouple `canonicalStringify` from `ObjectCanon` (#11254) Co-authored-by: Lenz Weber-Tronic --- .api-reports/api-report-cache.md | 12 +- .api-reports/api-report-core.md | 6 +- .api-reports/api-report-utilities.md | 25 ++-- .api-reports/api-report.md | 6 +- .changeset/beige-geese-wink.md | 5 + .size-limit.cjs | 4 +- src/__tests__/__snapshots__/exports.ts.snap | 1 + src/cache/index.ts | 8 +- src/cache/inmemory/__tests__/key-extractor.ts | 2 +- src/cache/inmemory/inMemoryCache.ts | 2 +- src/cache/inmemory/object-canon.ts | 32 ----- src/cache/inmemory/policies.ts | 6 - src/cache/inmemory/readFromStore.ts | 3 +- src/cache/inmemory/writeToStore.ts | 2 +- .../common/__tests__/canonicalStringify.ts | 110 ++++++++++++++++++ src/utilities/common/canonicalStringify.ts | 86 ++++++++++++++ src/utilities/graphql/storeUtils.ts | 43 +++---- src/utilities/index.ts | 1 + 18 files changed, 256 insertions(+), 98 deletions(-) create mode 100644 .changeset/beige-geese-wink.md create mode 100644 src/utilities/common/__tests__/canonicalStringify.ts create mode 100644 src/utilities/common/canonicalStringify.ts diff --git a/.api-reports/api-report-cache.md b/.api-reports/api-report-cache.md index e2135907143..0af8cb6cff1 100644 --- a/.api-reports/api-report-cache.md +++ b/.api-reports/api-report-cache.md @@ -192,7 +192,7 @@ export const cacheSlot: { // @public (undocumented) export const canonicalStringify: ((value: any) => string) & { - reset: typeof resetCanonicalStringify; + reset(): void; }; // @public (undocumented) @@ -858,9 +858,6 @@ export interface Reference { readonly __ref: string; } -// @public (undocumented) -function resetCanonicalStringify(): void; - // @public (undocumented) type SafeReadonly = T extends object ? Readonly : T; @@ -947,10 +944,9 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // -// src/cache/inmemory/object-canon.ts:203:32 - (ae-forgotten-export) The symbol "resetCanonicalStringify" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:98:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:92:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:126:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index e3138c81d29..a2acd522195 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -2179,9 +2179,9 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // -// src/cache/inmemory/policies.ts:98:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:92:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:126:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:112:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index cc97fb9dbe6..a90cd6f0b36 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -462,6 +462,11 @@ const enum CacheWriteBehavior { OVERWRITE = 1 } +// @public (undocumented) +export const canonicalStringify: ((value: any) => string) & { + reset(): void; +}; + // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; @@ -1073,7 +1078,7 @@ export function getQueryDefinition(doc: DocumentNode): OperationDefinitionNode; // @public (undocumented) export const getStoreKeyName: ((fieldName: string, args?: Record | null, directives?: Directives) => string) & { - setStringify(s: typeof stringify): (value: any) => string; + setStringify(s: typeof storeKeyNameStringify): (value: any) => string; }; // @public (undocumented) @@ -2284,6 +2289,9 @@ type StorageType = Record; // @public (undocumented) export function storeKeyNameFromField(field: FieldNode, variables?: Object): string; +// @public (undocumented) +let storeKeyNameStringify: (value: any) => string; + // @public (undocumented) export interface StoreObject { // (undocumented) @@ -2298,9 +2306,6 @@ type StoreObjectValueMaybeReference = StoreVal extends Record string; - // @public (undocumented) export function stringifyForDisplay(value: any, space?: number): string; @@ -2498,11 +2503,11 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // // src/cache/core/types/DataProxy.ts:141:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:63:3 - (ae-forgotten-export) The symbol "TypePolicy" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:168:3 - (ae-forgotten-export) The symbol "FieldReadFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:169:3 - (ae-forgotten-export) The symbol "FieldMergeFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:57:3 - (ae-forgotten-export) The symbol "TypePolicy" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "FieldReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:163:3 - (ae-forgotten-export) The symbol "FieldMergeFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:126:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/writeToStore.ts:65:7 - (ae-forgotten-export) The symbol "MergeTree" needs to be exported by the entry point index.d.ts // src/core/ApolloClient.ts:47:3 - (ae-forgotten-export) The symbol "UriFunction" needs to be exported by the entry point index.d.ts @@ -2517,7 +2522,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:205:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:191:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/storeUtils.ts:202:12 - (ae-forgotten-export) The symbol "stringify" needs to be exported by the entry point index.d.ts +// src/utilities/graphql/storeUtils.ts:208:12 - (ae-forgotten-export) The symbol "storeKeyNameStringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 1fac5b82bf4..bbf1f3f2898 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -2858,9 +2858,9 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // -// src/cache/inmemory/policies.ts:98:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:92:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:126:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:112:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts diff --git a/.changeset/beige-geese-wink.md b/.changeset/beige-geese-wink.md new file mode 100644 index 00000000000..d92e77ccb9d --- /dev/null +++ b/.changeset/beige-geese-wink.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Decouple `canonicalStringify` from `ObjectCanon` for better time and memory performance. diff --git a/.size-limit.cjs b/.size-limit.cjs index 090bc4c9dc4..37f130e22f1 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "38000", + limit: "38049", }, { path: "dist/main.cjs", @@ -10,7 +10,7 @@ const checks = [ { path: "dist/index.js", import: "{ ApolloClient, InMemoryCache, HttpLink }", - limit: "32052", + limit: "32082", }, ...[ "ApolloProvider", diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 6c06e405fd9..1c48ea242e3 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -395,6 +395,7 @@ Array [ "canUseSymbol", "canUseWeakMap", "canUseWeakSet", + "canonicalStringify", "checkDocument", "cloneDeep", "compact", diff --git a/src/cache/index.ts b/src/cache/index.ts index bed4ad849fd..d57341ff2ac 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -14,7 +14,11 @@ export type { export { MissingFieldError } from "./core/types/common.js"; export type { Reference } from "../utilities/index.js"; -export { isReference, makeReference } from "../utilities/index.js"; +export { + isReference, + makeReference, + canonicalStringify, +} from "../utilities/index.js"; export { EntityStore } from "./inmemory/entityStore.js"; export { @@ -38,8 +42,6 @@ export type { } from "./inmemory/policies.js"; export { Policies } from "./inmemory/policies.js"; -export { canonicalStringify } from "./inmemory/object-canon.js"; - export type { FragmentRegistryAPI } from "./inmemory/fragmentRegistry.js"; export { createFragmentRegistry } from "./inmemory/fragmentRegistry.js"; diff --git a/src/cache/inmemory/__tests__/key-extractor.ts b/src/cache/inmemory/__tests__/key-extractor.ts index 3636f6490e6..d525263d010 100644 --- a/src/cache/inmemory/__tests__/key-extractor.ts +++ b/src/cache/inmemory/__tests__/key-extractor.ts @@ -1,5 +1,5 @@ import { KeySpecifier } from "../policies"; -import { canonicalStringify } from "../object-canon"; +import { canonicalStringify } from "../../../utilities"; import { getSpecifierPaths, collectSpecifierPaths, diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index 2dac460bf54..3fd230b94e0 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -16,6 +16,7 @@ import { addTypenameToDocument, isReference, DocumentTransform, + canonicalStringify, } from "../../utilities/index.js"; import type { InMemoryCacheConfig, NormalizedCacheObject } from "./types.js"; import { StoreReader } from "./readFromStore.js"; @@ -24,7 +25,6 @@ import { EntityStore, supportsResultCaching } from "./entityStore.js"; import { makeVar, forgetCache, recallCache } from "./reactiveVars.js"; import { Policies } from "./policies.js"; import { hasOwn, normalizeConfig, shouldCanonizeResults } from "./helpers.js"; -import { canonicalStringify } from "./object-canon.js"; import type { OperationVariables } from "../../core/index.js"; type BroadcastOptions = Pick< diff --git a/src/cache/inmemory/object-canon.ts b/src/cache/inmemory/object-canon.ts index 376b474f282..05067b74d46 100644 --- a/src/cache/inmemory/object-canon.ts +++ b/src/cache/inmemory/object-canon.ts @@ -195,35 +195,3 @@ type SortedKeysInfo = { sorted: string[]; json: string; }; - -// Since the keys of canonical objects are always created in lexicographically -// sorted order, we can use the ObjectCanon to implement a fast and stable -// version of JSON.stringify, which automatically sorts object keys. -export const canonicalStringify = Object.assign( - function (value: any): string { - if (isObjectOrArray(value)) { - if (stringifyCanon === void 0) { - resetCanonicalStringify(); - } - const canonical = stringifyCanon.admit(value); - let json = stringifyCache.get(canonical); - if (json === void 0) { - stringifyCache.set(canonical, (json = JSON.stringify(canonical))); - } - return json; - } - return JSON.stringify(value); - }, - { - reset: resetCanonicalStringify, - } -); - -// Can be reset by calling canonicalStringify.reset(). -let stringifyCanon: ObjectCanon; -let stringifyCache: WeakMap; - -function resetCanonicalStringify() { - stringifyCanon = new ObjectCanon(); - stringifyCache = new (canUseWeakMap ? WeakMap : Map)(); -} diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index 2b2caec7e9c..6918c7dd6aa 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -48,17 +48,11 @@ import type { } from "../core/types/common.js"; import type { WriteContext } from "./writeToStore.js"; -// Upgrade to a faster version of the default stable JSON.stringify function -// used by getStoreKeyName. This function is used when computing storeFieldName -// strings (when no keyArgs has been configured for a field). -import { canonicalStringify } from "./object-canon.js"; import { keyArgsFnFromSpecifier, keyFieldsFnFromSpecifier, } from "./key-extractor.js"; -getStoreKeyName.setStringify(canonicalStringify); - export type TypePolicies = { [__typename: string]: TypePolicy; }; diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index 9a9ddc1498c..0643233b947 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -28,6 +28,7 @@ import { isNonNullObject, canUseWeakMap, compact, + canonicalStringify, } from "../../utilities/index.js"; import type { Cache } from "../core/types/Cache.js"; import type { @@ -50,7 +51,7 @@ import type { Policies } from "./policies.js"; import type { InMemoryCache } from "./inMemoryCache.js"; import type { MissingTree } from "../core/types/common.js"; import { MissingFieldError } from "../core/types/common.js"; -import { canonicalStringify, ObjectCanon } from "./object-canon.js"; +import { ObjectCanon } from "./object-canon.js"; export type VariableMap = { [name: string]: any }; diff --git a/src/cache/inmemory/writeToStore.ts b/src/cache/inmemory/writeToStore.ts index 1d3f3fb01d4..95057861b2f 100644 --- a/src/cache/inmemory/writeToStore.ts +++ b/src/cache/inmemory/writeToStore.ts @@ -25,6 +25,7 @@ import { addTypenameToDocument, isNonEmptyArray, argumentsObjectFromField, + canonicalStringify, } from "../../utilities/index.js"; import type { @@ -44,7 +45,6 @@ import type { StoreReader } from "./readFromStore.js"; import type { InMemoryCache } from "./inMemoryCache.js"; import type { EntityStore } from "./entityStore.js"; import type { Cache } from "../../core/index.js"; -import { canonicalStringify } from "./object-canon.js"; import { normalizeReadFieldOptions } from "./policies.js"; import type { ReadFieldFunction } from "../core/types/common.js"; diff --git a/src/utilities/common/__tests__/canonicalStringify.ts b/src/utilities/common/__tests__/canonicalStringify.ts new file mode 100644 index 00000000000..a82f18e3863 --- /dev/null +++ b/src/utilities/common/__tests__/canonicalStringify.ts @@ -0,0 +1,110 @@ +import { canonicalStringify } from "../canonicalStringify"; + +function forEachPermutation( + keys: string[], + callback: (permutation: string[]) => void +) { + if (keys.length <= 1) { + callback(keys); + return; + } + const first = keys[0]; + const rest = keys.slice(1); + forEachPermutation(rest, (permutation) => { + for (let i = 0; i <= permutation.length; ++i) { + callback([...permutation.slice(0, i), first, ...permutation.slice(i)]); + } + }); +} + +function allObjectPermutations>(obj: T) { + const keys = Object.keys(obj); + const permutations: T[] = []; + forEachPermutation(keys, (permutation) => { + const permutationObj = Object.create(Object.getPrototypeOf(obj)); + permutation.forEach((key) => { + permutationObj[key] = obj[key]; + }); + permutations.push(permutationObj); + }); + return permutations; +} + +describe("canonicalStringify", () => { + beforeEach(() => { + canonicalStringify.reset(); + }); + + it("should not modify original object", () => { + const obj = { c: 3, a: 1, b: 2 }; + expect(canonicalStringify(obj)).toBe('{"a":1,"b":2,"c":3}'); + expect(Object.keys(obj)).toEqual(["c", "a", "b"]); + }); + + it("forEachPermutation should work", () => { + const permutations: string[][] = []; + forEachPermutation(["a", "b", "c"], (permutation) => { + permutations.push(permutation); + }); + expect(permutations).toEqual([ + ["a", "b", "c"], + ["b", "a", "c"], + ["b", "c", "a"], + ["a", "c", "b"], + ["c", "a", "b"], + ["c", "b", "a"], + ]); + }); + + it("canonicalStringify should stably stringify all permutations of an object", () => { + const unstableStrings = new Set(); + const stableStrings = new Set(); + + allObjectPermutations({ + c: 3, + a: 1, + b: 2, + }).forEach((obj) => { + unstableStrings.add(JSON.stringify(obj)); + stableStrings.add(canonicalStringify(obj)); + + expect(canonicalStringify(obj)).toBe('{"a":1,"b":2,"c":3}'); + + allObjectPermutations({ + z: "z", + y: ["y", obj, "why"], + x: "x", + }).forEach((parent) => { + expect(canonicalStringify(parent)).toBe( + '{"x":"x","y":["y",{"a":1,"b":2,"c":3},"why"],"z":"z"}' + ); + }); + }); + + expect(unstableStrings.size).toBe(6); + expect(stableStrings.size).toBe(1); + }); + + it("should not modify keys of custom-prototype objects", () => { + class Custom { + z = "z"; + y = "y"; + x = "x"; + b = "b"; + a = "a"; + c = "c"; + } + + const obj = { + z: "z", + x: "x", + y: new Custom(), + }; + + expect(Object.keys(obj.y)).toEqual(["z", "y", "x", "b", "a", "c"]); + + expect(canonicalStringify(obj)).toBe( + '{"x":"x","y":{"z":"z","y":"y","x":"x","b":"b","a":"a","c":"c"},"z":"z"}' + ); + }); +}); diff --git a/src/utilities/common/canonicalStringify.ts b/src/utilities/common/canonicalStringify.ts new file mode 100644 index 00000000000..021f23430e2 --- /dev/null +++ b/src/utilities/common/canonicalStringify.ts @@ -0,0 +1,86 @@ +/** + * Like JSON.stringify, but with object keys always sorted in the same order. + * + * To achieve performant sorting, this function uses a Map from JSON-serialized + * arrays of keys (in any order) to sorted arrays of the same keys, with a + * single sorted array reference shared by all permutations of the keys. + * + * As a drawback, this function will add a little bit more memory for every + * object encountered that has different (more, less, a different order of) keys + * than in the past. + * + * In a typical application, this extra memory usage should not play a + * significant role, as `canonicalStringify` will be called for only a limited + * number of object shapes, and the cache will not grow beyond a certain point. + * But in some edge cases, this could be a problem, so we provide + * canonicalStringify.reset() as a way of clearing the cache. + * */ +export const canonicalStringify = Object.assign( + function canonicalStringify(value: any): string { + return JSON.stringify(value, stableObjectReplacer); + }, + { + reset() { + // Clearing the sortingMap will reclaim all cached memory, without + // affecting the logical results of canonicalStringify, but potentially + // sacrificing performance until the cache is refilled. + sortingMap.clear(); + }, + } +); + +// Values are JSON-serialized arrays of object keys (in any order), and values +// are sorted arrays of the same keys. +const sortingMap = new Map(); + +// The JSON.stringify function takes an optional second argument called a +// replacer function. This function is called for each key-value pair in the +// object being stringified, and its return value is used instead of the +// original value. If the replacer function returns a new value, that value is +// stringified as JSON instead of the original value of the property. +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter +function stableObjectReplacer(key: string, value: any) { + if (value && typeof value === "object") { + const proto = Object.getPrototypeOf(value); + // We don't want to mess with objects that are not "plain" objects, which + // means their prototype is either Object.prototype or null. This check also + // prevents needlessly rearranging the indices of arrays. + if (proto === Object.prototype || proto === null) { + const keys = Object.keys(value); + // If keys is already sorted, let JSON.stringify serialize the original + // value instead of creating a new object with keys in the same order. + if (keys.every(everyKeyInOrder)) return value; + const unsortedKey = JSON.stringify(keys); + let sortedKeys = sortingMap.get(unsortedKey); + if (!sortedKeys) { + keys.sort(); + const sortedKey = JSON.stringify(keys); + // Checking for sortedKey in the sortingMap allows us to share the same + // sorted array reference for all permutations of the same set of keys. + sortedKeys = sortingMap.get(sortedKey) || keys; + sortingMap.set(unsortedKey, sortedKeys); + sortingMap.set(sortedKey, sortedKeys); + } + const sortedObject = Object.create(proto); + // Reassigning the keys in sorted order will cause JSON.stringify to + // serialize them in sorted order. + sortedKeys.forEach((key) => { + sortedObject[key] = value[key]; + }); + return sortedObject; + } + } + return value; +} + +// Since everything that happens in stableObjectReplacer benefits from being as +// efficient as possible, we use a static function as the callback for +// keys.every in order to test if the provided keys are already sorted without +// allocating extra memory for a callback. +function everyKeyInOrder( + key: string, + i: number, + keys: readonly string[] +): boolean { + return i === 0 || keys[i - 1] <= key; +} diff --git a/src/utilities/graphql/storeUtils.ts b/src/utilities/graphql/storeUtils.ts index 606c3667f00..c119a447c26 100644 --- a/src/utilities/graphql/storeUtils.ts +++ b/src/utilities/graphql/storeUtils.ts @@ -24,6 +24,7 @@ import type { import { isNonNullObject } from "../common/objects.js"; import type { FragmentMap } from "./fragments.js"; import { getFragmentFromSelection } from "./fragments.js"; +import { canonicalStringify } from "../common/canonicalStringify.js"; export interface Reference { readonly __ref: string; @@ -194,6 +195,11 @@ const KNOWN_DIRECTIVES: string[] = [ "nonreactive", ]; +// Default stable JSON.stringify implementation used by getStoreKeyName. Can be +// updated/replaced with something better by calling +// getStoreKeyName.setStringify(newStringifyFunction). +let storeKeyNameStringify: (value: any) => string = canonicalStringify; + export const getStoreKeyName = Object.assign( function ( fieldName: string, @@ -220,7 +226,9 @@ export const getStoreKeyName = Object.assign( filteredArgs[key] = args[key]; }); - return `${directives["connection"]["key"]}(${stringify(filteredArgs)})`; + return `${directives["connection"]["key"]}(${storeKeyNameStringify( + filteredArgs + )})`; } else { return directives["connection"]["key"]; } @@ -232,7 +240,7 @@ export const getStoreKeyName = Object.assign( // We can't use `JSON.stringify` here since it's non-deterministic, // and can lead to different store key names being created even though // the `args` object used during creation has the same properties/values. - const stringifiedArgs: string = stringify(args); + const stringifiedArgs: string = storeKeyNameStringify(args); completeFieldName += `(${stringifiedArgs})`; } @@ -240,7 +248,9 @@ export const getStoreKeyName = Object.assign( Object.keys(directives).forEach((key) => { if (KNOWN_DIRECTIVES.indexOf(key) !== -1) return; if (directives[key] && Object.keys(directives[key]).length) { - completeFieldName += `@${key}(${stringify(directives[key])})`; + completeFieldName += `@${key}(${storeKeyNameStringify( + directives[key] + )})`; } else { completeFieldName += `@${key}`; } @@ -250,35 +260,14 @@ export const getStoreKeyName = Object.assign( return completeFieldName; }, { - setStringify(s: typeof stringify) { - const previous = stringify; - stringify = s; + setStringify(s: typeof storeKeyNameStringify) { + const previous = storeKeyNameStringify; + storeKeyNameStringify = s; return previous; }, } ); -// Default stable JSON.stringify implementation. Can be updated/replaced with -// something better by calling getStoreKeyName.setStringify. -let stringify = function defaultStringify(value: any): string { - return JSON.stringify(value, stringifyReplacer); -}; - -function stringifyReplacer(_key: string, value: any): any { - if (isNonNullObject(value) && !Array.isArray(value)) { - value = Object.keys(value) - .sort() - .reduce( - (copy, key) => { - copy[key] = value[key]; - return copy; - }, - {} as Record - ); - } - return value; -} - export function argumentsObjectFromField( field: FieldNode | DirectiveNode, variables?: Record diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 6462f639fea..ea660b1cafe 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -121,6 +121,7 @@ export * from "./common/stringifyForDisplay.js"; export * from "./common/mergeOptions.js"; export * from "./common/incrementalResult.js"; +export { canonicalStringify } from "./common/canonicalStringify.js"; export { omitDeep } from "./common/omitDeep.js"; export { stripTypename } from "./common/stripTypename.js"; From ba5faa5ea8f1ffcefd543f80f1e0dd8e1c647fe5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:01:32 +0200 Subject: [PATCH 18/90] Version Packages (alpha) (#11273) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 2 +- CHANGELOG.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index b16d942c18b..97962fa8e7a 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -5,7 +5,7 @@ "@apollo/client": "3.8.3" }, "changesets": [ - "pretty-readers-lick", + "beige-geese-wink", "shaggy-ears-scream", "sour-sheep-walk", "yellow-flies-repeat" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dede21ad49..c91f9b1dbb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/client +## 3.9.0-alpha.2 + +### Patch Changes + +- [#11254](https://github.com/apollographql/apollo-client/pull/11254) [`d08970d34`](https://github.com/apollographql/apollo-client/commit/d08970d348cf4ad6d80c6baf85b4a4cd4034a3bb) Thanks [@benjamn](https://github.com/benjamn)! - Decouple `canonicalStringify` from `ObjectCanon` for better time and memory performance. + ## 3.9.0-alpha.1 ### Minor Changes @@ -15,6 +21,7 @@ - [#6701](https://github.com/apollographql/apollo-client/pull/6701) [`8d2b4e107`](https://github.com/apollographql/apollo-client/commit/8d2b4e107d7c21563894ced3a65d631183b58fd9) Thanks [@prowe](https://github.com/prowe)! - Ability to dynamically match mocks Adds support for a new property `MockedResponse.variableMatcher`: a predicate function that accepts a `variables` param. If `true`, the `variables` will be passed into the `ResultFunction` to help dynamically build a response. + ## 3.8.5 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 2ac7ea6229d..0d656a76bf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.1", + "version": "3.9.0-alpha.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.0-alpha.1", + "version": "3.9.0-alpha.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 632afaaacd9..b8cefcee3b9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.1", + "version": "3.9.0-alpha.2", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 701a6fb3aeadd5364459e1896b67813e9c848f82 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 17 Oct 2023 12:12:18 -0600 Subject: [PATCH 19/90] Bump size-limit --- .size-limit.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limit.cjs b/.size-limit.cjs index f0500b94809..2664ba16fa5 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "37989", + limit: "38010", }, { path: "dist/main.cjs", @@ -10,7 +10,7 @@ const checks = [ { path: "dist/index.js", import: "{ ApolloClient, InMemoryCache, HttpLink }", - limit: "32023", + limit: "32070", }, ...[ "ApolloProvider", From 3862f9ba9086394c4cf4c2ecd99e8e0f6cf44885 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 2 Nov 2023 11:14:44 +0100 Subject: [PATCH 20/90] Add a defaultContext property on ApolloClient (#11275) --- .api-reports/api-report-core.md | 14 +- .api-reports/api-report-react.md | 14 +- .api-reports/api-report-react_components.md | 14 +- .api-reports/api-report-react_context.md | 14 +- .api-reports/api-report-react_hoc.md | 14 +- .api-reports/api-report-react_hooks.md | 14 +- .api-reports/api-report-react_ssr.md | 14 +- .api-reports/api-report-testing.md | 14 +- .api-reports/api-report-testing_core.md | 14 +- .api-reports/api-report-utilities.md | 14 +- .api-reports/api-report.md | 14 +- .changeset/breezy-spiders-tap.md | 38 ++++ .size-limit.cjs | 4 +- src/core/ApolloClient.ts | 7 + src/core/QueryManager.ts | 10 +- src/core/__tests__/QueryManager/index.ts | 217 ++++++++++++++++++++ 16 files changed, 383 insertions(+), 47 deletions(-) create mode 100644 .changeset/breezy-spiders-tap.md diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index fd7fcc664d7..5fd2e468bf4 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -100,6 +100,8 @@ export class ApolloClient implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -169,6 +171,7 @@ export type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -1681,7 +1684,7 @@ export type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1692,6 +1695,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1702,6 +1706,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly defaultContext: Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) readonly documentTransform: DocumentTransform; @@ -2192,9 +2198,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:126:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:112:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:116:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:149:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:378:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:191:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 0e1e91c88b5..ff07bab0665 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -110,6 +110,8 @@ class ApolloClient implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -204,6 +206,7 @@ type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -1508,7 +1511,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1519,6 +1522,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1528,6 +1532,8 @@ class QueryManager { cache: ApolloCache; // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; + // (undocumented) + readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2208,9 +2214,9 @@ interface WatchQueryOptions implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -203,6 +205,7 @@ type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -1304,7 +1307,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1315,6 +1318,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1324,6 +1328,8 @@ class QueryManager { cache: ApolloCache; // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; + // (undocumented) + readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1740,9 +1746,9 @@ interface WatchQueryOptions implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -204,6 +206,7 @@ type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -1214,7 +1217,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1225,6 +1228,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1234,6 +1238,8 @@ class QueryManager { cache: ApolloCache; // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; + // (undocumented) + readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1638,9 +1644,9 @@ interface WatchQueryOptions implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -203,6 +205,7 @@ type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -1282,7 +1285,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1293,6 +1296,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1302,6 +1306,8 @@ class QueryManager { cache: ApolloCache; // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; + // (undocumented) + readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1682,9 +1688,9 @@ export function withSubscription implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -200,6 +202,7 @@ type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -1428,7 +1431,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1439,6 +1442,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1448,6 +1452,8 @@ class QueryManager { cache: ApolloCache; // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; + // (undocumented) + readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2099,9 +2105,9 @@ interface WatchQueryOptions implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -205,6 +207,7 @@ type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -1201,7 +1204,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1212,6 +1215,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1221,6 +1225,8 @@ class QueryManager { cache: ApolloCache; // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; + // (undocumented) + readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1625,9 +1631,9 @@ interface WatchQueryOptions implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -201,6 +203,7 @@ type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -1285,7 +1288,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1296,6 +1299,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1306,6 +1310,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly defaultContext: Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) readonly documentTransform: DocumentTransform; @@ -1684,9 +1690,9 @@ export function withWarningSpy(it: (...args: TArgs // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:112:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:116:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:149:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:378:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:158:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:160:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 8f7dd41a516..c4ee5f52cf0 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -105,6 +105,8 @@ class ApolloClient implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -200,6 +202,7 @@ type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -1239,7 +1242,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1250,6 +1253,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1259,6 +1263,8 @@ class QueryManager { cache: ApolloCache; // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; + // (undocumented) + readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1640,9 +1646,9 @@ export function withWarningSpy(it: (...args: TArgs // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:112:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:116:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:149:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:378:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:158:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:160:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index b3815818dfa..2ec8f95a895 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -119,6 +119,8 @@ class ApolloClient implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -212,6 +214,7 @@ type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -1949,7 +1952,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1960,6 +1963,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1969,6 +1973,8 @@ class QueryManager { cache: ApolloCache; // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; + // (undocumented) + readonly defaultContext: Partial; // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2519,9 +2525,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:112:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:116:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:149:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:378:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:158:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:160:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index caf740c5e7a..e48ae78fd4e 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -105,6 +105,8 @@ export class ApolloClient implements DataProxy { // (undocumented) clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; @@ -174,6 +176,7 @@ export type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -2062,7 +2065,7 @@ export type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -2073,6 +2076,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -2083,6 +2087,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly defaultContext: Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) readonly documentTransform: DocumentTransform; @@ -2877,9 +2883,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:126:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:112:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:116:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:149:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:378:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:191:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:24:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts diff --git a/.changeset/breezy-spiders-tap.md b/.changeset/breezy-spiders-tap.md new file mode 100644 index 00000000000..a8af04cea0d --- /dev/null +++ b/.changeset/breezy-spiders-tap.md @@ -0,0 +1,38 @@ +--- +"@apollo/client": patch +--- + +Add a `defaultContext` option and property on `ApolloClient`, e.g. for keeping track of changing auth tokens or dependency injection. + +This can be used e.g. in authentication scenarios, where a new token might be +generated outside of the link chain and should passed into the link chain. + +```js +import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'; +import { setContext } from '@apollo/client/link/context'; + +const httpLink = createHttpLink({ + uri: '/graphql', +}); + +const authLink = setContext((_, { headers, token }) => { + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : "", + } + } +}); + +const client = new ApolloClient({ + link: authLink.concat(httpLink), + cache: new InMemoryCache() +}); + +// somewhere else in your application +function onNewToken(newToken) { + // token can now be changed for future requests without need for a global + // variable, scoped ref or recreating the client + client.defaultContext.token = newToken +} +``` diff --git a/.size-limit.cjs b/.size-limit.cjs index 2664ba16fa5..031272a0c63 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "38010", + limit: "38062", }, { path: "dist/main.cjs", @@ -10,7 +10,7 @@ const checks = [ { path: "dist/index.js", import: "{ ApolloClient, InMemoryCache, HttpLink }", - limit: "32070", + limit: "32113", }, ...[ "ApolloProvider", diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 12b9e7e0e0e..c667286c36a 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -54,6 +54,7 @@ export type ApolloClientOptions = { connectToDevTools?: boolean; queryDeduplication?: boolean; defaultOptions?: DefaultOptions; + defaultContext?: Partial; assumeImmutableResults?: boolean; resolvers?: Resolvers | Resolvers[]; typeDefs?: string | string[] | DocumentNode | DocumentNode[]; @@ -150,6 +151,7 @@ export class ApolloClient implements DataProxy { __DEV__, queryDeduplication = true, defaultOptions, + defaultContext, assumeImmutableResults = cache.assumeImmutableResults, resolvers, typeDefs, @@ -199,6 +201,7 @@ export class ApolloClient implements DataProxy { cache: this.cache, link: this.link, defaultOptions: this.defaultOptions, + defaultContext, documentTransform, queryDeduplication, ssrMode, @@ -692,4 +695,8 @@ export class ApolloClient implements DataProxy { public setLink(newLink: ApolloLink) { this.link = this.queryManager.link = newLink; } + + public get defaultContext() { + return this.queryManager.defaultContext; + } } diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 0d7ef342454..f24e7f2947a 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -8,6 +8,7 @@ import { equal } from "@wry/equality"; import type { ApolloLink, FetchResult } from "../link/core/index.js"; import { execute } from "../link/core/index.js"; import { + compact, hasDirectives, isExecutionPatchIncrementalResult, isExecutionPatchResult, @@ -62,6 +63,7 @@ import type { InternalRefetchQueriesOptions, InternalRefetchQueriesResult, InternalRefetchQueriesMap, + DefaultContext, } from "./types.js"; import { LocalState } from "./LocalState.js"; @@ -106,6 +108,7 @@ export class QueryManager { public readonly assumeImmutableResults: boolean; public readonly documentTransform: DocumentTransform; public readonly ssrMode: boolean; + public readonly defaultContext: Partial; private queryDeduplication: boolean; private clientAwareness: Record = {}; @@ -137,6 +140,7 @@ export class QueryManager { clientAwareness = {}, localState, assumeImmutableResults = !!cache.assumeImmutableResults, + defaultContext, }: { cache: ApolloCache; link: ApolloLink; @@ -148,6 +152,7 @@ export class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }) { const defaultDocumentTransform = new DocumentTransform( (document) => this.cache.transformDocument(document), @@ -172,6 +177,7 @@ export class QueryManager { // selections and fragments from the fragment registry. .concat(defaultDocumentTransform) : defaultDocumentTransform; + this.defaultContext = defaultContext || Object.create(null); if ((this.onBroadcast = onBroadcast)) { this.mutationStore = Object.create(null); @@ -1154,7 +1160,9 @@ export class QueryManager { return asyncMap( this.getObservableFromLink( linkDocument, - options.context, + // explicitly a shallow merge so any class instances etc. a user might + // put in here will not be merged into each other. + compact(this.defaultContext, options.context), options.variables ), diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index e1eeb02893c..b9ec7fef183 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -6142,4 +6142,221 @@ describe("QueryManager", () => { } ); }); + + describe("defaultContext", () => { + let _: any; // trash variable to throw away values when destructuring + _ = _; // omit "'_' is declared but its value is never read." compiler warning + + it("ApolloClient and QueryManager share a `defaultContext` instance (default empty object)", () => { + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + }); + + expect(client.defaultContext).toBe(client["queryManager"].defaultContext); + }); + + it("ApolloClient and QueryManager share a `defaultContext` instance (provided option)", () => { + const defaultContext = {}; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + defaultContext, + }); + + expect(client.defaultContext).toBe(defaultContext); + expect(client["queryManager"].defaultContext).toBe(defaultContext); + }); + + it("`defaultContext` cannot be reassigned on the user-facing `ApolloClient`", () => { + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + }); + + expect(() => { + // @ts-ignore + client.defaultContext = { query: { fetchPolicy: "cache-only" } }; + }).toThrowError(/Cannot set property defaultContext/); + }); + + it("`defaultContext` will be applied to the context of a query", async () => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultContext: { + foo: "bar", + }, + }); + + await client.query({ + query: gql` + query { + foo + } + `, + }); + + expect(context.foo).toBe("bar"); + }); + + it("`ApolloClient.defaultContext` can be modified and changes will show up in future queries", async () => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultContext: { + foo: "bar", + }, + }); + + // one query to "warm up" with an old value to make sure the value + // isn't locked in at the first query or something + await client.query({ + query: gql` + query { + foo + } + `, + }); + + expect(context.foo).toBe("bar"); + + client.defaultContext.foo = "changed"; + + await client.query({ + query: gql` + query { + foo + } + `, + }); + + expect(context.foo).toBe("changed"); + }); + + it("`defaultContext` will be shallowly merged with explicit context", async () => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultContext: { + foo: { bar: "baz" }, + a: { b: "c" }, + }, + }); + + await client.query({ + query: gql` + query { + foo + } + `, + context: { + a: { x: "y" }, + }, + }); + + expect(context).toEqual( + expect.objectContaining({ + foo: { bar: "baz" }, + a: { b: undefined, x: "y" }, + }) + ); + }); + + it("`defaultContext` will be shallowly merged with context from `defaultOptions.query.context", async () => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultContext: { + foo: { bar: "baz" }, + a: { b: "c" }, + }, + defaultOptions: { + query: { + context: { + a: { x: "y" }, + }, + }, + }, + }); + + await client.query({ + query: gql` + query { + foo + } + `, + }); + + expect(context.foo).toStrictEqual({ bar: "baz" }); + expect(context.a).toStrictEqual({ x: "y" }); + }); + + it( + "document existing behavior: `defaultOptions.query.context` will be " + + "completely overwritten by, not merged with, explicit context", + async () => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultOptions: { + query: { + context: { + foo: { bar: "baz" }, + }, + }, + }, + }); + + await client.query({ + query: gql` + query { + foo + } + `, + context: { + a: { x: "y" }, + }, + }); + + expect(context.a).toStrictEqual({ x: "y" }); + expect(context.foo).toBeUndefined(); + } + ); + }); }); From 46ab032af83a01f184bfcce5edba4b55dbb2962a Mon Sep 17 00:00:00 2001 From: Alessia Bellisario Date: Thu, 2 Nov 2023 09:48:44 -0400 Subject: [PATCH 21/90] feat: add multipart subscription network adapters for Relay and urql (#11301) --- ...pi-report-utilities_subscriptions_relay.md | 28 +++++++++ ...api-report-utilities_subscriptions_urql.md | 25 ++++++++ .changeset/strong-terms-perform.md | 46 ++++++++++++++ config/apiExtractor.ts | 2 +- config/entryPoints.js | 2 + package-lock.json | 9 ++- package.json | 1 + src/__tests__/__snapshots__/exports.ts.snap | 6 ++ src/__tests__/exports.ts | 7 +++ src/utilities/subscriptions/relay/index.ts | 60 +++++++++++++++++++ src/utilities/subscriptions/shared.ts | 21 +++++++ src/utilities/subscriptions/urql/index.ts | 56 +++++++++++++++++ 12 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 .api-reports/api-report-utilities_subscriptions_relay.md create mode 100644 .api-reports/api-report-utilities_subscriptions_urql.md create mode 100644 .changeset/strong-terms-perform.md create mode 100644 src/utilities/subscriptions/relay/index.ts create mode 100644 src/utilities/subscriptions/shared.ts create mode 100644 src/utilities/subscriptions/urql/index.ts diff --git a/.api-reports/api-report-utilities_subscriptions_relay.md b/.api-reports/api-report-utilities_subscriptions_relay.md new file mode 100644 index 00000000000..4e77625a6e1 --- /dev/null +++ b/.api-reports/api-report-utilities_subscriptions_relay.md @@ -0,0 +1,28 @@ +## API Report File for "@apollo/client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { GraphQLResponse } from 'relay-runtime'; +import { Observable } from 'relay-runtime'; +import type { RequestParameters } from 'relay-runtime'; + +// Warning: (ae-forgotten-export) The symbol "CreateMultipartSubscriptionOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function createFetchMultipartSubscription(uri: string, { fetch: preferredFetch, headers }?: CreateMultipartSubscriptionOptions): (operation: RequestParameters, variables: OperationVariables) => Observable; + +// @public (undocumented) +type CreateMultipartSubscriptionOptions = { + fetch?: WindowOrWorkerGlobalScope["fetch"]; + headers?: Record; +}; + +// @public (undocumented) +type OperationVariables = Record; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/.api-reports/api-report-utilities_subscriptions_urql.md b/.api-reports/api-report-utilities_subscriptions_urql.md new file mode 100644 index 00000000000..833fe4492b5 --- /dev/null +++ b/.api-reports/api-report-utilities_subscriptions_urql.md @@ -0,0 +1,25 @@ +## API Report File for "@apollo/client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Observable } from 'zen-observable-ts'; + +// Warning: (ae-forgotten-export) The symbol "CreateMultipartSubscriptionOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function createFetchMultipartSubscription(uri: string, { fetch: preferredFetch, headers }?: CreateMultipartSubscriptionOptions): ({ query, variables, }: { + query?: string | undefined; + variables: undefined | Record; +}) => Observable; + +// @public (undocumented) +type CreateMultipartSubscriptionOptions = { + fetch?: WindowOrWorkerGlobalScope["fetch"]; + headers?: Record; +}; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/.changeset/strong-terms-perform.md b/.changeset/strong-terms-perform.md new file mode 100644 index 00000000000..6974100076e --- /dev/null +++ b/.changeset/strong-terms-perform.md @@ -0,0 +1,46 @@ +--- +"@apollo/client": minor +--- + +Add multipart subscription network adapters for Relay and urql + +### Relay + +```tsx +import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/relay"; +import { Environment, Network, RecordSource, Store } from "relay-runtime"; + +const fetchMultipartSubs = createFetchMultipartSubscription( + "http://localhost:4000" +); + +const network = Network.create(fetchQuery, fetchMultipartSubs); + +export const RelayEnvironment = new Environment({ + network, + store: new Store(new RecordSource()), +}); +``` + +### Urql + +```tsx +import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/urql"; +import { Client, fetchExchange, subscriptionExchange } from "@urql/core"; + +const url = "http://localhost:4000"; + +const multipartSubscriptionForwarder = createFetchMultipartSubscription( + url +); + +const client = new Client({ + url, + exchanges: [ + fetchExchange, + subscriptionExchange({ + forwardSubscription: multipartSubscriptionForwarder, + }), + ], +}); +``` diff --git a/config/apiExtractor.ts b/config/apiExtractor.ts index c8d6dd86ec0..9ccb35cbf6b 100644 --- a/config/apiExtractor.ts +++ b/config/apiExtractor.ts @@ -30,7 +30,7 @@ map((entryPoint: { dirs: string[] }) => { enabled: true, ...baseConfig.apiReport, reportFileName: `api-report${ - path ? "-" + path.replace("/", "_") : "" + path ? "-" + path.replace(/\//g, "_") : "" }.md`, }, }, diff --git a/config/entryPoints.js b/config/entryPoints.js index dbd41ad4d64..3cd167c045e 100644 --- a/config/entryPoints.js +++ b/config/entryPoints.js @@ -27,6 +27,8 @@ const entryPoints = [ { dirs: ["testing"], extensions: [".js", ".jsx"] }, { dirs: ["testing", "core"] }, { dirs: ["utilities"] }, + { dirs: ["utilities", "subscriptions", "relay"] }, + { dirs: ["utilities", "subscriptions", "urql"] }, { dirs: ["utilities", "globals"], sideEffects: true }, ]; diff --git a/package-lock.json b/package-lock.json index b9c037271f0..c03bfde63f7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "@types/node-fetch": "2.6.7", "@types/react": "18.2.33", "@types/react-dom": "18.2.14", + "@types/relay-runtime": "14.1.14", "@types/use-sync-external-store": "0.0.5", "@typescript-eslint/eslint-plugin": "6.7.5", "@typescript-eslint/parser": "6.7.5", @@ -100,7 +101,7 @@ "whatwg-fetch": "3.6.19" }, "engines": { - "npm": "^7.20.3 || ^8.0.0 || ^9.0.0" + "npm": "^7.20.3 || ^8.0.0 || ^9.0.0 || ^10.0.0" }, "peerDependencies": { "graphql": "^15.0.0 || ^16.0.0", @@ -2950,6 +2951,12 @@ "@types/react": "*" } }, + "node_modules/@types/relay-runtime": { + "version": "14.1.14", + "resolved": "https://registry.npmjs.org/@types/relay-runtime/-/relay-runtime-14.1.14.tgz", + "integrity": "sha512-uG5GJhlyhqBp4j5b4xeH9LFMAr+xAFbWf1Q4ZLa0aLFJJNbjDVmHbzqzuXb+WqNpM3V7LaKwPB1m7w3NYSlCMg==", + "dev": true + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", diff --git a/package.json b/package.json index c2b886572a5..ed22fbaab9d 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "@types/node-fetch": "2.6.7", "@types/react": "18.2.33", "@types/react-dom": "18.2.14", + "@types/relay-runtime": "14.1.14", "@types/use-sync-external-store": "0.0.5", "@typescript-eslint/eslint-plugin": "6.7.5", "@typescript-eslint/parser": "6.7.5", diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 1c48ea242e3..1d9a73e0eb7 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -478,3 +478,9 @@ Array [ "newInvariantError", ] `; + +exports[`exports of public entry points @apollo/client/utilities/subscriptions/urql 1`] = ` +Array [ + "createFetchMultipartSubscription", +] +`; diff --git a/src/__tests__/exports.ts b/src/__tests__/exports.ts index 819ec6d2c81..dc46f2498ad 100644 --- a/src/__tests__/exports.ts +++ b/src/__tests__/exports.ts @@ -32,6 +32,7 @@ import * as testing from "../testing"; import * as testingCore from "../testing/core"; import * as utilities from "../utilities"; import * as utilitiesGlobals from "../utilities/globals"; +import * as urqlUtilities from "../utilities/subscriptions/urql"; const entryPoints = require("../../config/entryPoints.js"); @@ -76,11 +77,17 @@ describe("exports of public entry points", () => { check("@apollo/client/testing/core", testingCore); check("@apollo/client/utilities", utilities); check("@apollo/client/utilities/globals", utilitiesGlobals); + check("@apollo/client/utilities/subscriptions/urql", urqlUtilities); it("completeness", () => { const { join } = require("path").posix; entryPoints.forEach((info: Record) => { const id = join("@apollo/client", ...info.dirs); + // We don't want to add a devDependency for relay-runtime, + // and our API extractor job is already validating its public exports, + // so we'll skip the utilities/subscriptions/relay entrypoing here + // since it errors on the `relay-runtime` import. + if (id === "@apollo/client/utilities/subscriptions/relay") return; expect(testedIds).toContain(id); }); }); diff --git a/src/utilities/subscriptions/relay/index.ts b/src/utilities/subscriptions/relay/index.ts new file mode 100644 index 00000000000..94a6c250e19 --- /dev/null +++ b/src/utilities/subscriptions/relay/index.ts @@ -0,0 +1,60 @@ +import { Observable } from "relay-runtime"; +import type { RequestParameters, GraphQLResponse } from "relay-runtime"; +import { + handleError, + readMultipartBody, +} from "../../../link/http/parseAndCheckHttpResponse.js"; +import { maybe } from "../../index.js"; +import { serializeFetchParameter } from "../../../core/index.js"; + +import type { OperationVariables } from "../../../core/index.js"; +import type { Body } from "../../../link/http/selectHttpOptionsAndBody.js"; +import { generateOptionsForMultipartSubscription } from "../shared.js"; +import type { CreateMultipartSubscriptionOptions } from "../shared.js"; + +const backupFetch = maybe(() => fetch); + +export function createFetchMultipartSubscription( + uri: string, + { fetch: preferredFetch, headers }: CreateMultipartSubscriptionOptions = {} +) { + return function fetchMultipartSubscription( + operation: RequestParameters, + variables: OperationVariables + ): Observable { + const body: Body = { + operationName: operation.name, + variables, + query: operation.text || "", + }; + const options = generateOptionsForMultipartSubscription(headers || {}); + + return Observable.create((sink) => { + try { + options.body = serializeFetchParameter(body, "Payload"); + } catch (parseError) { + sink.error(parseError); + } + + const currentFetch = preferredFetch || maybe(() => fetch) || backupFetch; + const observerNext = sink.next.bind(sink); + + currentFetch!(uri, options) + .then((response) => { + const ctype = response.headers?.get("content-type"); + + if (ctype !== null && /^multipart\/mixed/i.test(ctype)) { + return readMultipartBody(response, observerNext); + } + + sink.error(new Error("Expected multipart response")); + }) + .then(() => { + sink.complete(); + }) + .catch((err: any) => { + handleError(err, sink); + }); + }); + }; +} diff --git a/src/utilities/subscriptions/shared.ts b/src/utilities/subscriptions/shared.ts new file mode 100644 index 00000000000..f3706dab11e --- /dev/null +++ b/src/utilities/subscriptions/shared.ts @@ -0,0 +1,21 @@ +import { fallbackHttpConfig } from "../../link/http/selectHttpOptionsAndBody.js"; + +export type CreateMultipartSubscriptionOptions = { + fetch?: WindowOrWorkerGlobalScope["fetch"]; + headers?: Record; +}; + +export function generateOptionsForMultipartSubscription( + headers: Record +) { + const options: { headers: Record; body?: string } = { + ...fallbackHttpConfig.options, + headers: { + ...(headers || {}), + ...fallbackHttpConfig.headers, + accept: + "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", + }, + }; + return options; +} diff --git a/src/utilities/subscriptions/urql/index.ts b/src/utilities/subscriptions/urql/index.ts new file mode 100644 index 00000000000..26bf4d4fb57 --- /dev/null +++ b/src/utilities/subscriptions/urql/index.ts @@ -0,0 +1,56 @@ +import { Observable } from "../../index.js"; +import { + handleError, + readMultipartBody, +} from "../../../link/http/parseAndCheckHttpResponse.js"; +import { maybe } from "../../index.js"; +import { serializeFetchParameter } from "../../../core/index.js"; +import type { Body } from "../../../link/http/selectHttpOptionsAndBody.js"; +import { generateOptionsForMultipartSubscription } from "../shared.js"; +import type { CreateMultipartSubscriptionOptions } from "../shared.js"; + +const backupFetch = maybe(() => fetch); + +export function createFetchMultipartSubscription( + uri: string, + { fetch: preferredFetch, headers }: CreateMultipartSubscriptionOptions = {} +) { + return function multipartSubscriptionForwarder({ + query, + variables, + }: { + query?: string; + variables: undefined | Record; + }) { + const body: Body = { variables, query }; + const options = generateOptionsForMultipartSubscription(headers || {}); + + return new Observable((observer) => { + try { + options.body = serializeFetchParameter(body, "Payload"); + } catch (parseError) { + observer.error(parseError); + } + + const currentFetch = preferredFetch || maybe(() => fetch) || backupFetch; + const observerNext = observer.next.bind(observer); + + currentFetch!(uri, options) + .then((response) => { + const ctype = response.headers?.get("content-type"); + + if (ctype !== null && /^multipart\/mixed/i.test(ctype)) { + return readMultipartBody(response, observerNext); + } + + observer.error(new Error("Expected multipart response")); + }) + .then(() => { + observer.complete(); + }) + .catch((err: any) => { + handleError(err, observer); + }); + }); + }; +} From be2029b7c929aadb53073a05b4615638016d82f8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 2 Nov 2023 10:00:15 -0400 Subject: [PATCH 22/90] Version Packages (alpha) (#11336) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 3 ++ CHANGELOG.md | 84 +++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 +-- package.json | 2 +- 4 files changed, 90 insertions(+), 3 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 97962fa8e7a..ba0a83ecf37 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -6,8 +6,11 @@ }, "changesets": [ "beige-geese-wink", + "breezy-spiders-tap", + "good-experts-repair", "shaggy-ears-scream", "sour-sheep-walk", + "strong-terms-perform", "yellow-flies-repeat" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a3c2bcc7ab..f15a55ef9cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,89 @@ # @apollo/client +## 3.9.0-alpha.3 + +### Minor Changes + +- [#11301](https://github.com/apollographql/apollo-client/pull/11301) [`46ab032af`](https://github.com/apollographql/apollo-client/commit/46ab032af83a01f184bfcce5edba4b55dbb2962a) Thanks [@alessbell](https://github.com/alessbell)! - Add multipart subscription network adapters for Relay and urql + + ### Relay + + ```tsx + import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/relay"; + import { Environment, Network, RecordSource, Store } from "relay-runtime"; + + const fetchMultipartSubs = createFetchMultipartSubscription( + "http://localhost:4000" + ); + + const network = Network.create(fetchQuery, fetchMultipartSubs); + + export const RelayEnvironment = new Environment({ + network, + store: new Store(new RecordSource()), + }); + ``` + + ### Urql + + ```tsx + import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/urql"; + import { Client, fetchExchange, subscriptionExchange } from "@urql/core"; + + const url = "http://localhost:4000"; + + const multipartSubscriptionForwarder = createFetchMultipartSubscription(url); + + const client = new Client({ + url, + exchanges: [ + fetchExchange, + subscriptionExchange({ + forwardSubscription: multipartSubscriptionForwarder, + }), + ], + }); + ``` + +### Patch Changes + +- [#11275](https://github.com/apollographql/apollo-client/pull/11275) [`3862f9ba9`](https://github.com/apollographql/apollo-client/commit/3862f9ba9086394c4cf4c2ecd99e8e0f6cf44885) Thanks [@phryneas](https://github.com/phryneas)! - Add a `defaultContext` option and property on `ApolloClient`, e.g. for keeping track of changing auth tokens or dependency injection. + + This can be used e.g. in authentication scenarios, where a new token might be + generated outside of the link chain and should passed into the link chain. + + ```js + import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"; + import { setContext } from "@apollo/client/link/context"; + + const httpLink = createHttpLink({ + uri: "/graphql", + }); + + const authLink = setContext((_, { headers, token }) => { + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : "", + }, + }; + }); + + const client = new ApolloClient({ + link: authLink.concat(httpLink), + cache: new InMemoryCache(), + }); + + // somewhere else in your application + function onNewToken(newToken) { + // token can now be changed for future requests without need for a global + // variable, scoped ref or recreating the client + client.defaultContext.token = newToken; + } + ``` + +- [#11297](https://github.com/apollographql/apollo-client/pull/11297) [`c8c76a522`](https://github.com/apollographql/apollo-client/commit/c8c76a522e593de0d06cff73fde2d9e88152bed6) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add an explicit return type for the `useReadQuery` hook called `UseReadQueryResult`. Previously the return type of this hook was inferred from the return value. + ## 3.9.0-alpha.2 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index c03bfde63f7..e80382241a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.2", + "version": "3.9.0-alpha.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.0-alpha.2", + "version": "3.9.0-alpha.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ed22fbaab9d..d355f3076f9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.2", + "version": "3.9.0-alpha.3", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 776631de4500d56252f6f5fdaf29a81c41dfbdc7 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 7 Nov 2023 10:22:33 +0100 Subject: [PATCH 23/90] Add `reset` method to `print`, hook up to `InMemoryCache.gc` (#11343) --- .api-reports/api-report-core.md | 5 ++-- .api-reports/api-report-link_batch-http.md | 5 ++-- .api-reports/api-report-link_http.md | 5 ++-- .api-reports/api-report-utilities.md | 5 ++-- .api-reports/api-report.md | 5 ++-- .changeset/wild-dolphins-jog.md | 5 ++++ .size-limit.cjs | 4 +-- src/cache/inmemory/inMemoryCache.ts | 2 ++ src/utilities/graphql/print.ts | 29 +++++++++++++++------- 9 files changed, 44 insertions(+), 21 deletions(-) create mode 100644 .changeset/wild-dolphins-jog.md diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 5fd2e468bf4..935e04317d7 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -20,7 +20,6 @@ import { InvariantError } from 'ts-invariant'; import { Observable } from 'zen-observable-ts'; import type { Subscription as ObservableSubscription } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import { print as print_3 } from 'graphql'; import { resetCaches } from 'graphql-tag'; import type { SelectionSetNode } from 'graphql'; import { setVerbosity as setLogVerbosity } from 'ts-invariant'; @@ -1614,7 +1613,9 @@ export type PossibleTypesMap = { }; // @public (undocumented) -const print_2: typeof print_3; +const print_2: ((ast: ASTNode) => string) & { + reset(): void; +}; // @public (undocumented) interface Printer { diff --git a/.api-reports/api-report-link_batch-http.md b/.api-reports/api-report-link_batch-http.md index 0e39566b61a..0081bc227be 100644 --- a/.api-reports/api-report-link_batch-http.md +++ b/.api-reports/api-report-link_batch-http.md @@ -10,7 +10,6 @@ import type { ExecutionResult } from 'graphql'; import type { GraphQLError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import { print as print_3 } from 'graphql'; // @public (undocumented) class ApolloLink { @@ -226,7 +225,9 @@ interface Operation { type Path = ReadonlyArray; // @public (undocumented) -const print_2: typeof print_3; +const print_2: ((ast: ASTNode) => string) & { + reset(): void; +}; // @public (undocumented) interface Printer { diff --git a/.api-reports/api-report-link_http.md b/.api-reports/api-report-link_http.md index 09b1e6459dd..d106d1f3e6b 100644 --- a/.api-reports/api-report-link_http.md +++ b/.api-reports/api-report-link_http.md @@ -11,7 +11,6 @@ import type { GraphQLError } from 'graphql'; import { InvariantError } from 'ts-invariant'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import { print as print_3 } from 'graphql'; // @public (undocumented) class ApolloLink { @@ -261,7 +260,9 @@ export function parseAndCheckHttpResponse(operations: Operation | Operation[]): type Path = ReadonlyArray; // @public (undocumented) -const print_2: typeof print_3; +const print_2: ((ast: ASTNode) => string) & { + reset(): void; +}; // @public (undocumented) interface Printer { diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 2ec8f95a895..4661d99d1f3 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -20,7 +20,6 @@ import { Observable } from 'zen-observable-ts'; import type { Subscription as ObservableSubscription } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type { OperationDefinitionNode } from 'graphql'; -import { print as print_3 } from 'graphql'; import type { SelectionNode } from 'graphql'; import type { SelectionSetNode } from 'graphql'; import type { Subscriber } from 'zen-observable-ts'; @@ -1882,7 +1881,9 @@ type PossibleTypesMap = { type Primitive = null | undefined | string | number | boolean | symbol | bigint; // @public (undocumented) -const print_2: typeof print_3; +const print_2: ((ast: ASTNode) => string) & { + reset(): void; +}; export { print_2 as print } // Warning: (ae-forgotten-export) The symbol "PendingPromise" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index e48ae78fd4e..227e554222e 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -22,7 +22,6 @@ import { InvariantError } from 'ts-invariant'; import { Observable } from 'zen-observable-ts'; import type { Subscription as ObservableSubscription } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import { print as print_3 } from 'graphql'; import * as React_2 from 'react'; import { ReactNode } from 'react'; import { resetCaches } from 'graphql-tag'; @@ -1952,7 +1951,9 @@ export type PossibleTypesMap = { type Primitive = null | undefined | string | number | boolean | symbol | bigint; // @public (undocumented) -const print_2: typeof print_3; +const print_2: ((ast: ASTNode) => string) & { + reset(): void; +}; // @public (undocumented) interface Printer { diff --git a/.changeset/wild-dolphins-jog.md b/.changeset/wild-dolphins-jog.md new file mode 100644 index 00000000000..8030414fffe --- /dev/null +++ b/.changeset/wild-dolphins-jog.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Add `reset` method to `print`, hook up to `InMemoryCache.gc` diff --git a/.size-limit.cjs b/.size-limit.cjs index 031272a0c63..5637af876f6 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "38062", + limit: "38074", }, { path: "dist/main.cjs", @@ -10,7 +10,7 @@ const checks = [ { path: "dist/index.js", import: "{ ApolloClient, InMemoryCache, HttpLink }", - limit: "32113", + limit: "32132", }, ...[ "ApolloProvider", diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index 3fd230b94e0..3fe7ab56fdf 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -17,6 +17,7 @@ import { isReference, DocumentTransform, canonicalStringify, + print, } from "../../utilities/index.js"; import type { InMemoryCacheConfig, NormalizedCacheObject } from "./types.js"; import { StoreReader } from "./readFromStore.js"; @@ -293,6 +294,7 @@ export class InMemoryCache extends ApolloCache { resetResultIdentities?: boolean; }) { canonicalStringify.reset(); + print.reset(); const ids = this.optimisticData.gc(); if (options && !this.txCount) { if (options.resetResultCache) { diff --git a/src/utilities/graphql/print.ts b/src/utilities/graphql/print.ts index 5fb1cb68599..d90a15611d0 100644 --- a/src/utilities/graphql/print.ts +++ b/src/utilities/graphql/print.ts @@ -1,14 +1,25 @@ +import type { ASTNode } from "graphql"; import { print as origPrint } from "graphql"; import { canUseWeakMap } from "../common/canUse.js"; -const printCache = canUseWeakMap ? new WeakMap() : undefined; -export const print: typeof origPrint = (ast) => { - let result; - result = printCache?.get(ast); +let printCache: undefined | WeakMap; +// further TODO: replace with `optimism` with a `WeakCache` once those are available +export const print = Object.assign( + (ast: ASTNode) => { + let result; + result = printCache?.get(ast); - if (!result) { - result = origPrint(ast); - printCache?.set(ast, result); + if (!result) { + result = origPrint(ast); + printCache?.set(ast, result); + } + return result; + }, + { + reset() { + printCache = canUseWeakMap ? new WeakMap() : undefined; + }, } - return result; -}; +); + +print.reset(); From d6d14911c40782cd6d69167b6f6169c890091ccb Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 8 Nov 2023 12:50:59 +0100 Subject: [PATCH 24/90] use external package to wrap `React` imports to circumvent non-existing functions in RSC (#11175) Co-authored-by: Jerel Miller --- .api-reports/api-report-react.md | 21 +++++++--------- .api-reports/api-report-react_components.md | 15 ++++++----- .api-reports/api-report-react_context.md | 21 +++++++--------- .api-reports/api-report-react_hoc.md | 14 +++++------ .api-reports/api-report-react_ssr.md | 20 ++++++--------- .api-reports/api-report.md | 21 +++++++--------- .changeset/friendly-clouds-laugh.md | 7 ++++++ .size-limit.cjs | 2 +- config/apiExtractor.ts | 25 ++++++++++++++++++- package-lock.json | 18 +++++++++++++ package.json | 1 + src/react/components/Mutation.tsx | 3 ++- src/react/components/Query.tsx | 5 +++- src/react/components/Subscription.tsx | 5 +++- src/react/components/types.ts | 12 ++++++--- src/react/context/ApolloConsumer.tsx | 7 +++--- src/react/context/ApolloContext.ts | 5 ++-- src/react/context/ApolloProvider.tsx | 7 +++--- src/react/hoc/graphql.tsx | 5 ++-- src/react/hoc/hoc-utils.tsx | 2 +- src/react/hoc/mutation-hoc.tsx | 7 +++--- src/react/hoc/query-hoc.tsx | 7 +++--- src/react/hoc/subscription-hoc.tsx | 7 +++--- src/react/hoc/withApollo.tsx | 13 ++++++---- src/react/hooks/internal/__use.ts | 2 +- src/react/hooks/internal/useDeepMemo.ts | 2 +- .../internal/useIsomorphicLayoutEffect.ts | 2 +- src/react/hooks/useApolloClient.ts | 2 +- src/react/hooks/useBackgroundQuery.ts | 2 +- src/react/hooks/useFragment.ts | 2 +- src/react/hooks/useLazyQuery.ts | 2 +- src/react/hooks/useMutation.ts | 2 +- src/react/hooks/useQuery.ts | 2 +- src/react/hooks/useReactiveVar.ts | 2 +- src/react/hooks/useReadQuery.ts | 2 +- src/react/hooks/useSubscription.ts | 2 +- src/react/hooks/useSuspenseQuery.ts | 2 +- src/react/hooks/useSyncExternalStore.ts | 2 +- src/react/ssr/RenderPromises.ts | 5 ++-- src/react/ssr/getDataFromTree.ts | 9 ++++--- src/react/ssr/renderToStringWithData.ts | 4 +-- src/react/types/types.ts | 4 +-- 42 files changed, 179 insertions(+), 121 deletions(-) create mode 100644 .changeset/friendly-clouds-laugh.md diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index ff07bab0665..ceee7f54aac 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; import type { ExecutionResult } from 'graphql'; @@ -15,8 +13,7 @@ import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import * as React_2 from 'react'; -import { ReactNode } from 'react'; +import type * as ReactTypes from 'react'; import type { Subscriber } from 'zen-observable-ts'; import type { Subscription } from 'zen-observable-ts'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; @@ -219,14 +216,14 @@ type ApolloClientOptions = { // Warning: (ae-forgotten-export) The symbol "ApolloConsumerProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const ApolloConsumer: React_2.FC; +export const ApolloConsumer: ReactTypes.FC; // @public (undocumented) interface ApolloConsumerProps { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // // (undocumented) - children: (client: ApolloClient) => React_2.ReactChild | null; + children: (client: ApolloClient) => ReactTypes.ReactChild | null; } // @public (undocumented) @@ -320,12 +317,12 @@ class ApolloLink { // Warning: (ae-forgotten-export) The symbol "ApolloProviderProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const ApolloProvider: React_2.FC>; +export const ApolloProvider: ReactTypes.FC>; // @public (undocumented) interface ApolloProviderProps { // (undocumented) - children: React_2.ReactNode | React_2.ReactNode[] | null; + children: ReactTypes.ReactNode | ReactTypes.ReactNode[] | null; // (undocumented) client: ApolloClient; } @@ -857,7 +854,7 @@ interface FragmentMap { type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; // @public (undocumented) -export function getApolloContext(): React_2.Context; +export function getApolloContext(): ReactTypes.Context; // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -1420,7 +1417,7 @@ interface QueryData { // @public (undocumented) export interface QueryDataOptions extends QueryFunctionOptions { // (undocumented) - children?: (result: QueryResult) => ReactNode; + children?: (result: QueryResult) => ReactTypes.ReactNode; // (undocumented) query: DocumentNode | TypedDocumentNode; } @@ -1780,11 +1777,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) class RenderPromises { // (undocumented) - addObservableQueryPromise(obsQuery: ObservableQuery): ReactNode; + addObservableQueryPromise(obsQuery: ObservableQuery): ReactTypes.ReactNode; // Warning: (ae-forgotten-export) The symbol "QueryData" needs to be exported by the entry point index.d.ts // // (undocumented) - addQueryPromise(queryInstance: QueryData, finish?: () => React.ReactNode): React.ReactNode; + addQueryPromise(queryInstance: QueryData, finish?: () => ReactTypes.ReactNode): ReactTypes.ReactNode; // (undocumented) consumeAndAwaitPromises(): Promise; // (undocumented) diff --git a/.api-reports/api-report-react_components.md b/.api-reports/api-report-react_components.md index bfb472ede13..ab9ab7432fb 100644 --- a/.api-reports/api-report-react_components.md +++ b/.api-reports/api-report-react_components.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; import type { ExecutionResult } from 'graphql'; @@ -16,6 +14,7 @@ import type { GraphQLErrorExtensions } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import * as PropTypes from 'prop-types'; +import type * as ReactTypes from 'react'; import type { Subscriber } from 'zen-observable-ts'; import type { Subscription as Subscription_2 } from 'zen-observable-ts'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; @@ -915,7 +914,7 @@ type Modifiers = Record> = Partia }>; // @public (undocumented) -export function Mutation(props: MutationComponentOptions): JSX.Element | null; +export function Mutation(props: MutationComponentOptions): ReactTypes.JSX.Element | null; // @public (undocumented) export namespace Mutation { @@ -967,7 +966,7 @@ export interface MutationComponentOptions, result: MutationResult) => JSX.Element | null; + children: (mutateFunction: MutationFunction, result: MutationResult) => ReactTypes.JSX.Element | null; // (undocumented) mutation: DocumentNode | TypedDocumentNode; } @@ -1205,7 +1204,7 @@ type OperationVariables = Record; type Path = ReadonlyArray; // @public (undocumented) -export function Query(props: QueryComponentOptions): JSX.Element | null; +export function Query(props: QueryComponentOptions): ReactTypes.JSX.Element | null; // @public (undocumented) export namespace Query { @@ -1226,7 +1225,7 @@ export interface QueryComponentOptions) => JSX.Element | null; + children: (result: QueryResult) => ReactTypes.JSX.Element | null; // (undocumented) query: DocumentNode | TypedDocumentNode; } @@ -1615,7 +1614,7 @@ type SubscribeToMoreOptions(props: SubscriptionComponentOptions): JSX.Element | null; +export function Subscription(props: SubscriptionComponentOptions): ReactTypes.JSX.Element | null; // @public (undocumented) export namespace Subscription { @@ -1634,7 +1633,7 @@ export interface Subscription { // @public (undocumented) export interface SubscriptionComponentOptions extends BaseSubscriptionOptions { // (undocumented) - children?: null | ((result: SubscriptionResult) => JSX.Element | null); + children?: null | ((result: SubscriptionResult) => ReactTypes.JSX.Element | null); // (undocumented) subscription: DocumentNode | TypedDocumentNode; } diff --git a/.api-reports/api-report-react_context.md b/.api-reports/api-report-react_context.md index 384e4137877..949a46e67e6 100644 --- a/.api-reports/api-report-react_context.md +++ b/.api-reports/api-report-react_context.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; import type { ExecutionResult } from 'graphql'; @@ -15,8 +13,7 @@ import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import * as React_2 from 'react'; -import { ReactNode } from 'react'; +import type * as ReactTypes from 'react'; import type { Subscriber } from 'zen-observable-ts'; import type { Subscription } from 'zen-observable-ts'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; @@ -217,14 +214,14 @@ type ApolloClientOptions = { }; // @public (undocumented) -export const ApolloConsumer: React_2.FC; +export const ApolloConsumer: ReactTypes.FC; // @public (undocumented) export interface ApolloConsumerProps { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // // (undocumented) - children: (client: ApolloClient) => React_2.ReactChild | null; + children: (client: ApolloClient) => ReactTypes.ReactChild | null; } // @public (undocumented) @@ -316,12 +313,12 @@ class ApolloLink { } // @public (undocumented) -export const ApolloProvider: React_2.FC>; +export const ApolloProvider: ReactTypes.FC>; // @public (undocumented) export interface ApolloProviderProps { // (undocumented) - children: React_2.ReactNode | React_2.ReactNode[] | null; + children: ReactTypes.ReactNode | ReactTypes.ReactNode[] | null; // (undocumented) client: ApolloClient; } @@ -723,7 +720,7 @@ interface FragmentMap { type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; // @public (undocumented) -export function getApolloContext(): React_2.Context; +export function getApolloContext(): ReactTypes.Context; // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -1136,7 +1133,7 @@ interface QueryDataOptions) => ReactNode; + children?: (result: QueryResult) => ReactTypes.ReactNode; // (undocumented) query: DocumentNode | TypedDocumentNode; } @@ -1454,11 +1451,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) class RenderPromises { // (undocumented) - addObservableQueryPromise(obsQuery: ObservableQuery): ReactNode; + addObservableQueryPromise(obsQuery: ObservableQuery): ReactTypes.ReactNode; // Warning: (ae-forgotten-export) The symbol "QueryData" needs to be exported by the entry point index.d.ts // // (undocumented) - addQueryPromise(queryInstance: QueryData, finish?: () => React.ReactNode): React.ReactNode; + addQueryPromise(queryInstance: QueryData, finish?: () => ReactTypes.ReactNode): ReactTypes.ReactNode; // (undocumented) consumeAndAwaitPromises(): Promise; // Warning: (ae-forgotten-export) The symbol "QueryDataOptions" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react_hoc.md b/.api-reports/api-report-react_hoc.md index 3ed5c1b1873..bf4d99a6b7b 100644 --- a/.api-reports/api-report-react_hoc.md +++ b/.api-reports/api-report-react_hoc.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; import type { ExecutionResult } from 'graphql'; @@ -15,7 +13,7 @@ import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import * as React_2 from 'react'; +import type * as ReactTypes from 'react'; import type { Subscriber } from 'zen-observable-ts'; import type { Subscription } from 'zen-observable-ts'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; @@ -733,7 +731,7 @@ interface FragmentMap { type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; // @public (undocumented) -export function graphql> & Partial>>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: React.ComponentType) => React.ComponentClass; +export function graphql> & Partial>>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -1660,7 +1658,7 @@ interface WatchQueryOptions(WrappedComponent: React_2.ComponentType>>, operationOptions?: OperationOption): React_2.ComponentClass>; +export function withApollo(WrappedComponent: ReactTypes.ComponentType>>, operationOptions?: OperationOption): ReactTypes.ComponentClass>; // @public (undocumented) export type WithApolloClient

= P & { @@ -1668,13 +1666,13 @@ export type WithApolloClient

= P & { }; // @public (undocumented) -export function withMutation = {}, TGraphQLVariables extends OperationVariables = {}, TChildProps = MutateProps, TContext extends Record = DefaultContext, TCache extends ApolloCache = ApolloCache>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: React_2.ComponentType) => React_2.ComponentClass; +export function withMutation = {}, TGraphQLVariables extends OperationVariables = {}, TChildProps = MutateProps, TContext extends Record = DefaultContext, TCache extends ApolloCache = ApolloCache>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; // @public (undocumented) -export function withQuery = Record, TData extends object = {}, TGraphQLVariables extends object = {}, TChildProps extends object = DataProps>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: React_2.ComponentType) => React_2.ComponentClass; +export function withQuery = Record, TData extends object = {}, TGraphQLVariables extends object = {}, TChildProps extends object = DataProps>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; // @public (undocumented) -export function withSubscription>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: React_2.ComponentType) => React_2.ComponentClass; +export function withSubscription>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; // Warnings were encountered during analysis: // diff --git a/.api-reports/api-report-react_ssr.md b/.api-reports/api-report-react_ssr.md index bfee4799898..9f6a2cdaf8e 100644 --- a/.api-reports/api-report-react_ssr.md +++ b/.api-reports/api-report-react_ssr.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; import type { ExecutionResult } from 'graphql'; @@ -15,9 +13,7 @@ import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import * as React_2 from 'react'; -import type { ReactElement } from 'react'; -import { ReactNode } from 'react'; +import type * as ReactTypes from 'react'; import type { Subscriber } from 'zen-observable-ts'; import type { Subscription } from 'zen-observable-ts'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; @@ -694,7 +690,7 @@ interface FragmentMap { type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; // @public (undocumented) -export function getDataFromTree(tree: React_2.ReactNode, context?: { +export function getDataFromTree(tree: ReactTypes.ReactNode, context?: { [key: string]: any; }): Promise; @@ -705,11 +701,11 @@ export function getMarkupFromTree({ tree, context, renderFunction, }: GetMarkupF // @public (undocumented) type GetMarkupFromTreeOptions = { - tree: React_2.ReactNode; + tree: ReactTypes.ReactNode; context?: { [key: string]: any; }; - renderFunction?: (tree: React_2.ReactElement) => string | PromiseLike; + renderFunction?: (tree: ReactTypes.ReactElement) => string | PromiseLike; }; // @public (undocumented) @@ -1123,7 +1119,7 @@ interface QueryDataOptions) => ReactNode; + children?: (result: QueryResult) => ReactTypes.ReactNode; // (undocumented) query: DocumentNode | TypedDocumentNode; } @@ -1441,11 +1437,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) export class RenderPromises { // (undocumented) - addObservableQueryPromise(obsQuery: ObservableQuery): ReactNode; + addObservableQueryPromise(obsQuery: ObservableQuery): ReactTypes.ReactNode; // Warning: (ae-forgotten-export) The symbol "QueryData" needs to be exported by the entry point index.d.ts // // (undocumented) - addQueryPromise(queryInstance: QueryData, finish?: () => React.ReactNode): React.ReactNode; + addQueryPromise(queryInstance: QueryData, finish?: () => ReactTypes.ReactNode): ReactTypes.ReactNode; // (undocumented) consumeAndAwaitPromises(): Promise; // Warning: (ae-forgotten-export) The symbol "QueryDataOptions" needs to be exported by the entry point index.d.ts @@ -1461,7 +1457,7 @@ export class RenderPromises { } // @public (undocumented) -export function renderToStringWithData(component: ReactElement): Promise; +export function renderToStringWithData(component: ReactTypes.ReactElement): Promise; // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 227e554222e..a9d2cce7e79 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import { disableExperimentalFragmentVariables } from 'graphql-tag'; import { disableFragmentWarnings } from 'graphql-tag'; @@ -22,8 +20,7 @@ import { InvariantError } from 'ts-invariant'; import { Observable } from 'zen-observable-ts'; import type { Subscription as ObservableSubscription } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import * as React_2 from 'react'; -import { ReactNode } from 'react'; +import type * as ReactTypes from 'react'; import { resetCaches } from 'graphql-tag'; import type { SelectionSetNode } from 'graphql'; import { setVerbosity as setLogVerbosity } from 'ts-invariant'; @@ -188,12 +185,12 @@ export type ApolloClientOptions = { // Warning: (ae-forgotten-export) The symbol "ApolloConsumerProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const ApolloConsumer: React_2.FC; +export const ApolloConsumer: ReactTypes.FC; // @public (undocumented) interface ApolloConsumerProps { // (undocumented) - children: (client: ApolloClient) => React_2.ReactChild | null; + children: (client: ApolloClient) => ReactTypes.ReactChild | null; } // @public (undocumented) @@ -286,12 +283,12 @@ export interface ApolloPayloadResult, TExtensions = // Warning: (ae-forgotten-export) The symbol "ApolloProviderProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const ApolloProvider: React_2.FC>; +export const ApolloProvider: ReactTypes.FC>; // @public (undocumented) interface ApolloProviderProps { // (undocumented) - children: React_2.ReactNode | React_2.ReactNode[] | null; + children: ReactTypes.ReactNode | ReactTypes.ReactNode[] | null; // (undocumented) client: ApolloClient; } @@ -1071,7 +1068,7 @@ export function fromError(errorValue: any): Observable; export function fromPromise(promise: Promise): Observable; // @public (undocumented) -export function getApolloContext(): React_2.Context; +export function getApolloContext(): ReactTypes.Context; export { gql } @@ -1977,7 +1974,7 @@ interface QueryData { // @public (undocumented) export interface QueryDataOptions extends QueryFunctionOptions { // (undocumented) - children?: (result: QueryResult) => ReactNode; + children?: (result: QueryResult) => ReactTypes.ReactNode; // (undocumented) query: DocumentNode | TypedDocumentNode; } @@ -2346,11 +2343,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) class RenderPromises { // (undocumented) - addObservableQueryPromise(obsQuery: ObservableQuery): ReactNode; + addObservableQueryPromise(obsQuery: ObservableQuery): ReactTypes.ReactNode; // Warning: (ae-forgotten-export) The symbol "QueryData" needs to be exported by the entry point index.d.ts // // (undocumented) - addQueryPromise(queryInstance: QueryData, finish?: () => React.ReactNode): React.ReactNode; + addQueryPromise(queryInstance: QueryData, finish?: () => ReactTypes.ReactNode): ReactTypes.ReactNode; // (undocumented) consumeAndAwaitPromises(): Promise; // (undocumented) diff --git a/.changeset/friendly-clouds-laugh.md b/.changeset/friendly-clouds-laugh.md new file mode 100644 index 00000000000..3821053fa83 --- /dev/null +++ b/.changeset/friendly-clouds-laugh.md @@ -0,0 +1,7 @@ +--- +"@apollo/client": minor +--- + +To work around issues in React Server Components, especially with bundling for +the Next.js "edge" runtime we now use an external package to wrap `react` imports +instead of importing React directly. diff --git a/.size-limit.cjs b/.size-limit.cjs index 5637af876f6..bd4497ec3a5 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "38074", + limit: "38101", }, { path: "dist/main.cjs", diff --git a/config/apiExtractor.ts b/config/apiExtractor.ts index 9ccb35cbf6b..12f712dfc77 100644 --- a/config/apiExtractor.ts +++ b/config/apiExtractor.ts @@ -6,6 +6,7 @@ import { } from "@microsoft/api-extractor"; // @ts-ignore import { map } from "./entryPoints.js"; +import { readFileSync } from "fs"; // Load and parse the api-extractor.json file const configObjectFullPath = path.resolve(__dirname, "../api-extractor.json"); @@ -43,13 +44,35 @@ map((entryPoint: { dirs: string[] }) => { showVerboseMessages: true, }); - if (extractorResult.succeeded) { + let succeededAdditionalChecks = true; + const contents = readFileSync(extractorConfig.reportFilePath, "utf8"); + + if (contents.includes("rehackt")) { + succeededAdditionalChecks = false; + console.error( + "❗ %s contains a reference to the `rehackt` package!", + extractorConfig.reportFilePath + ); + } + if (contents.includes('/// ')) { + succeededAdditionalChecks = false; + console.error( + "❗ %s contains a reference to the global `React` type!/n" + + 'Use `import type * as ReactTypes from "react";` instead', + extractorConfig.reportFilePath + ); + } + + if (extractorResult.succeeded && succeededAdditionalChecks) { console.log(`✅ API Extractor completed successfully`); } else { console.error( `❗ API Extractor completed with ${extractorResult.errorCount} errors` + ` and ${extractorResult.warningCount} warnings` ); + if (!succeededAdditionalChecks) { + console.error("Additional checks failed."); + } process.exitCode = 1; } }); diff --git a/package-lock.json b/package-lock.json index e80382241a3..c224c5575a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "hoist-non-react-statics": "^3.3.2", "optimism": "^0.17.5", "prop-types": "^15.7.2", + "rehackt": "0.0.3", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", @@ -9791,6 +9792,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rehackt": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.0.3.tgz", + "integrity": "sha512-aBRHudKhOWwsTvCbSoinzq+Lej/7R8e8UoPvLZo5HirZIIBLGAgdG7SL9QpdcBoQ7+3QYPi3lRLknAzXBlhZ7g==", + "peerDependencies": { + "@types/react": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", diff --git a/package.json b/package.json index d355f3076f9..43f1536e8d9 100644 --- a/package.json +++ b/package.json @@ -96,6 +96,7 @@ "hoist-non-react-statics": "^3.3.2", "optimism": "^0.17.5", "prop-types": "^15.7.2", + "rehackt": "0.0.3", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", diff --git a/src/react/components/Mutation.tsx b/src/react/components/Mutation.tsx index 492e0089312..8dca6889f7d 100644 --- a/src/react/components/Mutation.tsx +++ b/src/react/components/Mutation.tsx @@ -1,4 +1,5 @@ import * as PropTypes from "prop-types"; +import type * as ReactTypes from "react"; import type { OperationVariables } from "../../core/index.js"; import type { MutationComponentOptions } from "./types.js"; @@ -6,7 +7,7 @@ import { useMutation } from "../hooks/index.js"; export function Mutation( props: MutationComponentOptions -) { +): ReactTypes.JSX.Element | null { const [runMutation, result] = useMutation(props.mutation, props); return props.children ? props.children(runMutation, result) : null; } diff --git a/src/react/components/Query.tsx b/src/react/components/Query.tsx index 1e5611f646a..119696f3973 100644 --- a/src/react/components/Query.tsx +++ b/src/react/components/Query.tsx @@ -1,4 +1,5 @@ import * as PropTypes from "prop-types"; +import type * as ReactTypes from "react"; import type { OperationVariables } from "../../core/index.js"; import type { QueryComponentOptions } from "./types.js"; @@ -7,7 +8,9 @@ import { useQuery } from "../hooks/index.js"; export function Query< TData = any, TVariables extends OperationVariables = OperationVariables, ->(props: QueryComponentOptions) { +>( + props: QueryComponentOptions +): ReactTypes.JSX.Element | null { const { children, query, ...options } = props; const result = useQuery(query, options); return result ? children(result as any) : null; diff --git a/src/react/components/Subscription.tsx b/src/react/components/Subscription.tsx index 5701cdcb01d..76dda1a6241 100644 --- a/src/react/components/Subscription.tsx +++ b/src/react/components/Subscription.tsx @@ -1,4 +1,5 @@ import * as PropTypes from "prop-types"; +import type * as ReactTypes from "react"; import type { OperationVariables } from "../../core/index.js"; import type { SubscriptionComponentOptions } from "./types.js"; @@ -7,7 +8,9 @@ import { useSubscription } from "../hooks/index.js"; export function Subscription< TData = any, TVariables extends OperationVariables = OperationVariables, ->(props: SubscriptionComponentOptions) { +>( + props: SubscriptionComponentOptions +): ReactTypes.JSX.Element | null { const result = useSubscription(props.subscription, props); return props.children && result ? props.children(result) : null; } diff --git a/src/react/components/types.ts b/src/react/components/types.ts index 4e1abacb6a1..a0114f65ae2 100644 --- a/src/react/components/types.ts +++ b/src/react/components/types.ts @@ -1,6 +1,8 @@ import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; +import type * as ReactTypes from "react"; + import type { OperationVariables, DefaultContext, @@ -20,7 +22,9 @@ export interface QueryComponentOptions< TData = any, TVariables extends OperationVariables = OperationVariables, > extends QueryFunctionOptions { - children: (result: QueryResult) => JSX.Element | null; + children: ( + result: QueryResult + ) => ReactTypes.JSX.Element | null; query: DocumentNode | TypedDocumentNode; } @@ -34,7 +38,7 @@ export interface MutationComponentOptions< children: ( mutateFunction: MutationFunction, result: MutationResult - ) => JSX.Element | null; + ) => ReactTypes.JSX.Element | null; } export interface SubscriptionComponentOptions< @@ -42,5 +46,7 @@ export interface SubscriptionComponentOptions< TVariables extends OperationVariables = OperationVariables, > extends BaseSubscriptionOptions { subscription: DocumentNode | TypedDocumentNode; - children?: null | ((result: SubscriptionResult) => JSX.Element | null); + children?: + | null + | ((result: SubscriptionResult) => ReactTypes.JSX.Element | null); } diff --git a/src/react/context/ApolloConsumer.tsx b/src/react/context/ApolloConsumer.tsx index ac26e734da9..e71ec520ee3 100644 --- a/src/react/context/ApolloConsumer.tsx +++ b/src/react/context/ApolloConsumer.tsx @@ -1,15 +1,16 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { ApolloClient } from "../../core/index.js"; import { getApolloContext } from "./ApolloContext.js"; export interface ApolloConsumerProps { - children: (client: ApolloClient) => React.ReactChild | null; + children: (client: ApolloClient) => ReactTypes.ReactChild | null; } -export const ApolloConsumer: React.FC = (props) => { +export const ApolloConsumer: ReactTypes.FC = (props) => { const ApolloContext = getApolloContext(); return ( diff --git a/src/react/context/ApolloContext.ts b/src/react/context/ApolloContext.ts index e942e8e9dad..49a7f3885b8 100644 --- a/src/react/context/ApolloContext.ts +++ b/src/react/context/ApolloContext.ts @@ -1,4 +1,5 @@ -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { ApolloClient } from "../../core/index.js"; import { canUseSymbol } from "../../utilities/index.js"; import type { RenderPromises } from "../ssr/index.js"; @@ -17,7 +18,7 @@ const contextKey = canUseSymbol ? Symbol.for("__APOLLO_CONTEXT__") : "__APOLLO_CONTEXT__"; -export function getApolloContext(): React.Context { +export function getApolloContext(): ReactTypes.Context { invariant( "createContext" in React, "Invoking `getApolloContext` in an environment where `React.createContext` is not available.\n" + diff --git a/src/react/context/ApolloProvider.tsx b/src/react/context/ApolloProvider.tsx index 930e32dab0a..aa91b2cfc01 100644 --- a/src/react/context/ApolloProvider.tsx +++ b/src/react/context/ApolloProvider.tsx @@ -1,16 +1,17 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { ApolloClient } from "../../core/index.js"; import { getApolloContext } from "./ApolloContext.js"; export interface ApolloProviderProps { client: ApolloClient; - children: React.ReactNode | React.ReactNode[] | null; + children: ReactTypes.ReactNode | ReactTypes.ReactNode[] | null; } -export const ApolloProvider: React.FC> = ({ +export const ApolloProvider: ReactTypes.FC> = ({ client, children, }) => { diff --git a/src/react/hoc/graphql.tsx b/src/react/hoc/graphql.tsx index 88bf39e9961..1a770aae04e 100644 --- a/src/react/hoc/graphql.tsx +++ b/src/react/hoc/graphql.tsx @@ -1,4 +1,5 @@ import type { DocumentNode } from "graphql"; +import type * as ReactTypes from "react"; import { parser, DocumentType } from "../parser/index.js"; import { withQuery } from "./query-hoc.js"; @@ -22,8 +23,8 @@ export function graphql< TChildProps > = {} ): ( - WrappedComponent: React.ComponentType -) => React.ComponentClass { + WrappedComponent: ReactTypes.ComponentType +) => ReactTypes.ComponentClass { switch (parser(document).type) { case DocumentType.Mutation: return withMutation(document, operationOptions); diff --git a/src/react/hoc/hoc-utils.tsx b/src/react/hoc/hoc-utils.tsx index 2e59f74e944..7c7d0598e08 100644 --- a/src/react/hoc/hoc-utils.tsx +++ b/src/react/hoc/hoc-utils.tsx @@ -1,5 +1,5 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; import type { OperationVariables } from "../../core/index.js"; import type { IDocumentDefinition } from "../parser/index.js"; diff --git a/src/react/hoc/mutation-hoc.tsx b/src/react/hoc/mutation-hoc.tsx index a0098a0f290..9e0917d0b6e 100644 --- a/src/react/hoc/mutation-hoc.tsx +++ b/src/react/hoc/mutation-hoc.tsx @@ -1,4 +1,5 @@ -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { DocumentNode } from "graphql"; import hoistNonReactStatics from "hoist-non-react-statics"; @@ -56,8 +57,8 @@ export function withMutation< >; return ( - WrappedComponent: React.ComponentType - ): React.ComponentClass => { + WrappedComponent: ReactTypes.ComponentType + ): ReactTypes.ComponentClass => { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; class GraphQL extends GraphQLBase { static displayName = graphQLDisplayName; diff --git a/src/react/hoc/query-hoc.tsx b/src/react/hoc/query-hoc.tsx index 144133494c6..5587ce83119 100644 --- a/src/react/hoc/query-hoc.tsx +++ b/src/react/hoc/query-hoc.tsx @@ -1,4 +1,5 @@ -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { DocumentNode } from "graphql"; import hoistNonReactStatics from "hoist-non-react-statics"; @@ -50,8 +51,8 @@ export function withQuery< // allow for advanced referential equality checks let lastResultProps: TChildProps | void; return ( - WrappedComponent: React.ComponentType - ): React.ComponentClass => { + WrappedComponent: ReactTypes.ComponentType + ): ReactTypes.ComponentClass => { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; class GraphQL extends GraphQLBase { static displayName = graphQLDisplayName; diff --git a/src/react/hoc/subscription-hoc.tsx b/src/react/hoc/subscription-hoc.tsx index b044c1c2658..5bbea06bb3a 100644 --- a/src/react/hoc/subscription-hoc.tsx +++ b/src/react/hoc/subscription-hoc.tsx @@ -1,4 +1,5 @@ -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { DocumentNode } from "graphql"; import hoistNonReactStatics from "hoist-non-react-statics"; @@ -48,8 +49,8 @@ export function withSubscription< // allow for advanced referential equality checks let lastResultProps: TChildProps | void; return ( - WrappedComponent: React.ComponentType - ): React.ComponentClass => { + WrappedComponent: ReactTypes.ComponentType + ): ReactTypes.ComponentClass => { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; class GraphQL extends GraphQLBase< TProps, diff --git a/src/react/hoc/withApollo.tsx b/src/react/hoc/withApollo.tsx index bfa21b454a4..71b982a3cac 100644 --- a/src/react/hoc/withApollo.tsx +++ b/src/react/hoc/withApollo.tsx @@ -1,20 +1,21 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import hoistNonReactStatics from "hoist-non-react-statics"; import { ApolloConsumer } from "../context/index.js"; import type { OperationOption, WithApolloClient } from "./types.js"; -function getDisplayName

(WrappedComponent: React.ComponentType

) { +function getDisplayName

(WrappedComponent: ReactTypes.ComponentType

) { return WrappedComponent.displayName || WrappedComponent.name || "Component"; } export function withApollo( - WrappedComponent: React.ComponentType< + WrappedComponent: ReactTypes.ComponentType< WithApolloClient> >, operationOptions: OperationOption = {} -): React.ComponentClass> { +): ReactTypes.ComponentClass> { const withDisplayName = `withApollo(${getDisplayName(WrappedComponent)})`; class WithApollo extends React.Component> { @@ -39,7 +40,9 @@ export function withApollo( return this.wrappedInstance; } - setWrappedInstance(ref: React.ComponentType>) { + setWrappedInstance( + ref: ReactTypes.ComponentType> + ) { this.wrappedInstance = ref; } diff --git a/src/react/hooks/internal/__use.ts b/src/react/hooks/internal/__use.ts index 2a49fab9e0c..b06760ff04e 100644 --- a/src/react/hooks/internal/__use.ts +++ b/src/react/hooks/internal/__use.ts @@ -1,5 +1,5 @@ import { wrapPromiseWithState } from "../../../utilities/index.js"; -import * as React from "react"; +import * as React from "rehackt"; type Use = (promise: Promise) => T; // Prevent webpack from complaining about our feature detection of the diff --git a/src/react/hooks/internal/useDeepMemo.ts b/src/react/hooks/internal/useDeepMemo.ts index 5a49115a49b..916e23a746d 100644 --- a/src/react/hooks/internal/useDeepMemo.ts +++ b/src/react/hooks/internal/useDeepMemo.ts @@ -1,5 +1,5 @@ import type { DependencyList } from "react"; -import * as React from "react"; +import * as React from "rehackt"; import { equal } from "@wry/equality"; export function useDeepMemo( diff --git a/src/react/hooks/internal/useIsomorphicLayoutEffect.ts b/src/react/hooks/internal/useIsomorphicLayoutEffect.ts index f5380a88fdd..1d13ade2854 100644 --- a/src/react/hooks/internal/useIsomorphicLayoutEffect.ts +++ b/src/react/hooks/internal/useIsomorphicLayoutEffect.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import { canUseDOM } from "../../../utilities/index.js"; // use canUseDOM here instead of canUseLayoutEffect because we want to be able diff --git a/src/react/hooks/useApolloClient.ts b/src/react/hooks/useApolloClient.ts index c9e81a8551c..54bf7bd0874 100644 --- a/src/react/hooks/useApolloClient.ts +++ b/src/react/hooks/useApolloClient.ts @@ -1,5 +1,5 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; import type { ApolloClient } from "../../core/index.js"; import { getApolloContext } from "../context/index.js"; diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 8e49114e7aa..99a41f5e5eb 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import type { DocumentNode, OperationVariables, diff --git a/src/react/hooks/useFragment.ts b/src/react/hooks/useFragment.ts index debd1bd02eb..0f07d0df3a5 100644 --- a/src/react/hooks/useFragment.ts +++ b/src/react/hooks/useFragment.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import { equal } from "@wry/equality"; import type { DeepPartial } from "../../utilities/index.js"; diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 535b9b4ceb2..81f988ded50 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -1,6 +1,6 @@ import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; -import * as React from "react"; +import * as React from "rehackt"; import type { OperationVariables } from "../../core/index.js"; import { mergeOptions } from "../../utilities/index.js"; diff --git a/src/react/hooks/useMutation.ts b/src/react/hooks/useMutation.ts index f3c2b233abd..fe76e167218 100644 --- a/src/react/hooks/useMutation.ts +++ b/src/react/hooks/useMutation.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import type { diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 33b127f62e6..7ff9cb085ff 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -1,6 +1,6 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import { equal } from "@wry/equality"; diff --git a/src/react/hooks/useReactiveVar.ts b/src/react/hooks/useReactiveVar.ts index 2d14c12cd63..b98c4401e69 100644 --- a/src/react/hooks/useReactiveVar.ts +++ b/src/react/hooks/useReactiveVar.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import type { ReactiveVar } from "../../core/index.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index e6a97e1446f..803535c4878 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import { unwrapQueryRef } from "../cache/QueryReference.js"; import type { QueryReference } from "../cache/QueryReference.js"; import { __use } from "./internal/index.js"; diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts index 764879810da..ab98b2bb540 100644 --- a/src/react/hooks/useSubscription.ts +++ b/src/react/hooks/useSubscription.ts @@ -1,5 +1,5 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { equal } from "@wry/equality"; diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index 1139b3a4984..b92bfe2eea9 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import { invariant } from "../../utilities/globals/index.js"; import type { ApolloClient, diff --git a/src/react/hooks/useSyncExternalStore.ts b/src/react/hooks/useSyncExternalStore.ts index 0ae0a84d793..adf4d059f7f 100644 --- a/src/react/hooks/useSyncExternalStore.ts +++ b/src/react/hooks/useSyncExternalStore.ts @@ -1,5 +1,5 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; import { canUseLayoutEffect } from "../../utilities/index.js"; diff --git a/src/react/ssr/RenderPromises.ts b/src/react/ssr/RenderPromises.ts index d72574bd6c1..f51bcc93aca 100644 --- a/src/react/ssr/RenderPromises.ts +++ b/src/react/ssr/RenderPromises.ts @@ -1,4 +1,5 @@ import type { DocumentNode } from "graphql"; +import type * as ReactTypes from "react"; import type { ObservableQuery, OperationVariables } from "../../core/index.js"; import type { QueryDataOptions } from "../types/types.js"; @@ -58,8 +59,8 @@ export class RenderPromises { public addQueryPromise( queryInstance: QueryData, - finish?: () => React.ReactNode - ): React.ReactNode { + finish?: () => ReactTypes.ReactNode + ): ReactTypes.ReactNode { if (!this.stopped) { const info = this.lookupQueryInfo(queryInstance.getOptions()); if (!info.seen) { diff --git a/src/react/ssr/getDataFromTree.ts b/src/react/ssr/getDataFromTree.ts index 49d706934c6..b43e6c112cf 100644 --- a/src/react/ssr/getDataFromTree.ts +++ b/src/react/ssr/getDataFromTree.ts @@ -1,10 +1,11 @@ -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import { getApolloContext } from "../context/index.js"; import { RenderPromises } from "./RenderPromises.js"; import { renderToStaticMarkup } from "react-dom/server"; export function getDataFromTree( - tree: React.ReactNode, + tree: ReactTypes.ReactNode, context: { [key: string]: any } = {} ) { return getMarkupFromTree({ @@ -17,10 +18,10 @@ export function getDataFromTree( } export type GetMarkupFromTreeOptions = { - tree: React.ReactNode; + tree: ReactTypes.ReactNode; context?: { [key: string]: any }; renderFunction?: ( - tree: React.ReactElement + tree: ReactTypes.ReactElement ) => string | PromiseLike; }; diff --git a/src/react/ssr/renderToStringWithData.ts b/src/react/ssr/renderToStringWithData.ts index 0e3944344a2..f6bcb345849 100644 --- a/src/react/ssr/renderToStringWithData.ts +++ b/src/react/ssr/renderToStringWithData.ts @@ -1,9 +1,9 @@ -import type { ReactElement } from "react"; +import type * as ReactTypes from "react"; import { getMarkupFromTree } from "./getDataFromTree.js"; import { renderToString } from "react-dom/server"; export function renderToStringWithData( - component: ReactElement + component: ReactTypes.ReactElement ): Promise { return getMarkupFromTree({ tree: component, diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 5d057261dbc..70df3b03458 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -1,4 +1,4 @@ -import type { ReactNode } from "react"; +import type * as ReactTypes from "react"; import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; @@ -93,7 +93,7 @@ export interface QueryDataOptions< TData = any, TVariables extends OperationVariables = OperationVariables, > extends QueryFunctionOptions { - children?: (result: QueryResult) => ReactNode; + children?: (result: QueryResult) => ReactTypes.ReactNode; query: DocumentNode | TypedDocumentNode; } From 950cae884c4349a545ca8ed7aaa13cc5d70946e9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Nov 2023 16:38:03 +0100 Subject: [PATCH 25/90] Version Packages (alpha) (#11347) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 2 ++ CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index ba0a83ecf37..87706b33f54 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -7,10 +7,12 @@ "changesets": [ "beige-geese-wink", "breezy-spiders-tap", + "friendly-clouds-laugh", "good-experts-repair", "shaggy-ears-scream", "sour-sheep-walk", "strong-terms-perform", + "wild-dolphins-jog", "yellow-flies-repeat" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index f15a55ef9cb..054cc0a4438 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # @apollo/client +## 3.9.0-alpha.4 + +### Minor Changes + +- [#11175](https://github.com/apollographql/apollo-client/pull/11175) [`d6d14911c`](https://github.com/apollographql/apollo-client/commit/d6d14911c40782cd6d69167b6f6169c890091ccb) Thanks [@phryneas](https://github.com/phryneas)! - To work around issues in React Server Components, especially with bundling for + the Next.js "edge" runtime we now use an external package to wrap `react` imports + instead of importing React directly. + +### Patch Changes + +- [#11343](https://github.com/apollographql/apollo-client/pull/11343) [`776631de4`](https://github.com/apollographql/apollo-client/commit/776631de4500d56252f6f5fdaf29a81c41dfbdc7) Thanks [@phryneas](https://github.com/phryneas)! - Add `reset` method to `print`, hook up to `InMemoryCache.gc` + ## 3.9.0-alpha.3 ### Minor Changes diff --git a/package-lock.json b/package-lock.json index c224c5575a5..8956ec4bd09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.3", + "version": "3.9.0-alpha.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.0-alpha.3", + "version": "3.9.0-alpha.4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 43f1536e8d9..11e0780bc3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.3", + "version": "3.9.0-alpha.4", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From bd2667619700139af32a45364794d11f845ab6cf Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 9 Nov 2023 11:19:49 +0100 Subject: [PATCH 26/90] Add `reset` method to `DocumentTransform`, hook `InMemoryCache.addTypenameTransform` up to `InMemoryCache.gc` (#11344) Co-authored-by: Jerel Miller --- .api-reports/api-report-core.md | 2 ++ .api-reports/api-report-react.md | 4 +++- .api-reports/api-report-react_components.md | 4 +++- .api-reports/api-report-react_context.md | 4 +++- .api-reports/api-report-react_hoc.md | 4 +++- .api-reports/api-report-react_hooks.md | 4 +++- .api-reports/api-report-react_ssr.md | 4 +++- .api-reports/api-report-testing.md | 4 +++- .api-reports/api-report-testing_core.md | 4 +++- .api-reports/api-report-utilities.md | 2 ++ .api-reports/api-report.md | 2 ++ .changeset/hot-ducks-burn.md | 5 +++++ .size-limit.cjs | 4 ++-- src/cache/inmemory/inMemoryCache.ts | 1 + src/utilities/graphql/DocumentTransform.ts | 8 ++++++++ 15 files changed, 46 insertions(+), 10 deletions(-) create mode 100644 .changeset/hot-ducks-burn.md diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 935e04317d7..119bc538d4e 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -601,6 +601,8 @@ export class DocumentTransform { // (undocumented) static identity(): DocumentTransform; // (undocumented) + resetCache(): void; + // (undocumented) static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index ceee7f54aac..d7a73cbefd3 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -728,6 +728,8 @@ class DocumentTransform { // (undocumented) static identity(): DocumentTransform; // (undocumented) + resetCache(): void; + // (undocumented) static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; @@ -2221,7 +2223,7 @@ interface WatchQueryOptions boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; @@ -1753,7 +1755,7 @@ interface WatchQueryOptions boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; @@ -1649,7 +1651,7 @@ interface WatchQueryOptions boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; @@ -1694,7 +1696,7 @@ export function withSubscription boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; @@ -2115,7 +2117,7 @@ interface WatchQueryOptions boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; @@ -1635,7 +1637,7 @@ interface WatchQueryOptions boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; @@ -1698,7 +1700,7 @@ export function withWarningSpy(it: (...args: TArgs // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:205:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:191:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:122:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts +// src/utilities/graphql/DocumentTransform.ts:130:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index c4ee5f52cf0..db8ba43820e 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -581,6 +581,8 @@ class DocumentTransform { // (undocumented) static identity(): DocumentTransform; // (undocumented) + resetCache(): void; + // (undocumented) static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; @@ -1654,7 +1656,7 @@ export function withWarningSpy(it: (...args: TArgs // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:205:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:191:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:122:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts +// src/utilities/graphql/DocumentTransform.ts:130:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 4661d99d1f3..db99ac83b43 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -759,6 +759,8 @@ export class DocumentTransform { // (undocumented) static identity(): DocumentTransform; // (undocumented) + resetCache(): void; + // (undocumented) static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index a9d2cce7e79..174182d1f2c 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -751,6 +751,8 @@ export class DocumentTransform { // (undocumented) static identity(): DocumentTransform; // (undocumented) + resetCache(): void; + // (undocumented) static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; diff --git a/.changeset/hot-ducks-burn.md b/.changeset/hot-ducks-burn.md new file mode 100644 index 00000000000..c0f8ac1836c --- /dev/null +++ b/.changeset/hot-ducks-burn.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Add a `resetCache` method to `DocumentTransform` and hook `InMemoryCache.addTypenameTransform` up to `InMemoryCache.gc` diff --git a/.size-limit.cjs b/.size-limit.cjs index bd4497ec3a5..a15bd6aa160 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "38101", + limit: "38124", }, { path: "dist/main.cjs", @@ -10,7 +10,7 @@ const checks = [ { path: "dist/index.js", import: "{ ApolloClient, InMemoryCache, HttpLink }", - limit: "32132", + limit: "32162", }, ...[ "ApolloProvider", diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index 3fe7ab56fdf..1297b6eeb19 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -295,6 +295,7 @@ export class InMemoryCache extends ApolloCache { }) { canonicalStringify.reset(); print.reset(); + this.addTypenameTransform.resetCache(); const ids = this.optimisticData.gc(); if (options && !this.txCount) { if (options.resetResultCache) { diff --git a/src/utilities/graphql/DocumentTransform.ts b/src/utilities/graphql/DocumentTransform.ts index 0a614df4190..d88bcf7615b 100644 --- a/src/utilities/graphql/DocumentTransform.ts +++ b/src/utilities/graphql/DocumentTransform.ts @@ -80,6 +80,14 @@ export class DocumentTransform { } } + /** + * Resets the internal cache of this transform, if it has one. + */ + resetCache() { + this.stableCacheKeys = + this.stableCacheKeys && new Trie(canUseWeakMap, (key) => ({ key })); + } + transformDocument(document: DocumentNode) { // If a user passes an already transformed result back to this function, // immediately return it. From b0bf0d80ed0b1a2c3014b35286813915605c0ec7 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 14 Nov 2023 14:43:50 +0100 Subject: [PATCH 27/90] fix up type in 3.9 branch after merge (#11362) --- .api-reports/api-report-testing.md | 4 ++-- .api-reports/api-report-testing_core.md | 2 +- src/testing/core/mocking/mockLink.ts | 2 +- src/testing/react/MockedProvider.tsx | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index 7367367ab39..c65ea55027e 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -880,7 +880,7 @@ export interface MockedProviderProps { // (undocumented) link?: ApolloLink; // (undocumented) - mocks?: ReadonlyArray; + mocks?: ReadonlyArray>; // (undocumented) resolvers?: Resolvers; // (undocumented) @@ -925,7 +925,7 @@ interface MockedSubscriptionResult { // @public (undocumented) export class MockLink extends ApolloLink { - constructor(mockedResponses: ReadonlyArray, addTypename?: Boolean, options?: MockLinkOptions); + constructor(mockedResponses: ReadonlyArray>, addTypename?: Boolean, options?: MockLinkOptions); // (undocumented) addMockedResponse(mockedResponse: MockedResponse): void; // (undocumented) diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index a8494f44a28..5aaef2383c6 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -879,7 +879,7 @@ interface MockedSubscriptionResult { // @public (undocumented) export class MockLink extends ApolloLink { - constructor(mockedResponses: ReadonlyArray, addTypename?: Boolean, options?: MockLinkOptions); + constructor(mockedResponses: ReadonlyArray>, addTypename?: Boolean, options?: MockLinkOptions); // (undocumented) addMockedResponse(mockedResponse: MockedResponse): void; // (undocumented) diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index 7c48201eb73..723a78d5c4c 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -57,7 +57,7 @@ export class MockLink extends ApolloLink { private mockedResponsesByKey: { [key: string]: MockedResponse[] } = {}; constructor( - mockedResponses: ReadonlyArray, + mockedResponses: ReadonlyArray>, addTypename: Boolean = true, options: MockLinkOptions = Object.create(null) ) { diff --git a/src/testing/react/MockedProvider.tsx b/src/testing/react/MockedProvider.tsx index b7afb9d416c..bd70d34ccd1 100644 --- a/src/testing/react/MockedProvider.tsx +++ b/src/testing/react/MockedProvider.tsx @@ -11,7 +11,7 @@ import type { Resolvers } from "../../core/index.js"; import type { ApolloCache } from "../../cache/index.js"; export interface MockedProviderProps { - mocks?: ReadonlyArray; + mocks?: ReadonlyArray>; addTypename?: boolean; defaultOptions?: DefaultOptions; cache?: ApolloCache; From 7d8e18493cd13134726c6643cbf0fadb08be2d37 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 14 Nov 2023 15:17:33 +0100 Subject: [PATCH 28/90] `InMemoryCache.gc`: also trigger `FragmentRegistry.resetCaches` (#11355) --- .api-reports/api-report-cache.md | 2 ++ .api-reports/api-report-core.md | 2 ++ .api-reports/api-report-utilities.md | 2 ++ .api-reports/api-report.md | 2 ++ .changeset/violet-lions-draw.md | 5 +++++ .size-limit.cjs | 4 ++-- src/cache/inmemory/fragmentRegistry.ts | 1 + src/cache/inmemory/inMemoryCache.ts | 1 + 8 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 .changeset/violet-lions-draw.md diff --git a/.api-reports/api-report-cache.md b/.api-reports/api-report-cache.md index 5e2df27abe7..c84e7527da6 100644 --- a/.api-reports/api-report-cache.md +++ b/.api-reports/api-report-cache.md @@ -488,6 +488,8 @@ export interface FragmentRegistryAPI { // (undocumented) register(...fragments: DocumentNode[]): this; // (undocumented) + resetCaches(): void; + // (undocumented) transform(document: D): D; } diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 8028536b8dd..f8c570ceebc 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -885,6 +885,8 @@ interface FragmentRegistryAPI { // (undocumented) register(...fragments: DocumentNode[]): this; // (undocumented) + resetCaches(): void; + // (undocumented) transform(document: D): D; } diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 67a29e74257..93fab5008fc 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -1028,6 +1028,8 @@ interface FragmentRegistryAPI { // (undocumented) register(...fragments: DocumentNode[]): this; // (undocumented) + resetCaches(): void; + // (undocumented) transform(document: D): D; } diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index e02fd3d7b75..71c954da766 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -1057,6 +1057,8 @@ interface FragmentRegistryAPI { // (undocumented) register(...fragments: DocumentNode[]): this; // (undocumented) + resetCaches(): void; + // (undocumented) transform(document: D): D; } diff --git a/.changeset/violet-lions-draw.md b/.changeset/violet-lions-draw.md new file mode 100644 index 00000000000..6e5d046a6c9 --- /dev/null +++ b/.changeset/violet-lions-draw.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +InMemoryCache.gc now also triggers FragmentRegistry.resetCaches (if there is a FragmentRegistry) diff --git a/.size-limit.cjs b/.size-limit.cjs index f4ad9acaf4b..63819405fd1 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "38142", + limit: "38164", }, { path: "dist/main.cjs", @@ -10,7 +10,7 @@ const checks = [ { path: "dist/index.js", import: "{ ApolloClient, InMemoryCache, HttpLink }", - limit: "32167", + limit: "32188", }, ...[ "ApolloProvider", diff --git a/src/cache/inmemory/fragmentRegistry.ts b/src/cache/inmemory/fragmentRegistry.ts index 0832e6a7934..f7db169e3b0 100644 --- a/src/cache/inmemory/fragmentRegistry.ts +++ b/src/cache/inmemory/fragmentRegistry.ts @@ -16,6 +16,7 @@ export interface FragmentRegistryAPI { register(...fragments: DocumentNode[]): this; lookup(fragmentName: string): FragmentDefinitionNode | null; transform(document: D): D; + resetCaches(): void; } // As long as createFragmentRegistry is not imported or used, the diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index 1cce80d023b..a14090acf1d 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -296,6 +296,7 @@ export class InMemoryCache extends ApolloCache { canonicalStringify.reset(); print.reset(); this.addTypenameTransform.resetCache(); + this.config.fragments?.resetCaches(); const ids = this.optimisticData.gc(); if (options && !this.txCount) { if (options.resetResultCache) { From ac4e382898f0f207111eff42c3f21ec2869cb1ef Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 17 Nov 2023 15:53:37 +0100 Subject: [PATCH 29/90] update size-limit --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index f71b98fe37c..52108cf4fb9 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 37975, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32019 + "dist/apollo-client.min.cjs": 38164, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32188 } From ebd8fe2c1b8b50bfeb2da20aeca5671300fb5564 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 20 Nov 2023 17:38:13 +0100 Subject: [PATCH 30/90] Clarify types of `EntityStore.makeCacheKey` (#11371) --- .api-reports/api-report-cache.md | 6 ++++++ .api-reports/api-report-core.md | 6 ++++++ .api-reports/api-report-utilities.md | 6 ++++++ .api-reports/api-report.md | 6 ++++++ .changeset/thick-mice-collect.md | 5 +++++ src/cache/inmemory/entityStore.ts | 22 ++++++++++++++++++++++ src/cache/inmemory/readFromStore.ts | 4 ++++ 7 files changed, 55 insertions(+) create mode 100644 .changeset/thick-mice-collect.md diff --git a/.api-reports/api-report-cache.md b/.api-reports/api-report-cache.md index c84e7527da6..2efb3c2ed67 100644 --- a/.api-reports/api-report-cache.md +++ b/.api-reports/api-report-cache.md @@ -355,6 +355,12 @@ export abstract class EntityStore implements NormalizedCache { // (undocumented) protected lookup(dataId: string, dependOnExistence?: boolean): StoreObject | undefined; // (undocumented) + makeCacheKey(document: DocumentNode, callback: Cache_2.WatchCallback, details: string): object; + // (undocumented) + makeCacheKey(selectionSet: SelectionSetNode, parent: string | StoreObject, varString: string | undefined, canonizeResults: boolean): object; + // (undocumented) + makeCacheKey(field: FieldNode, array: readonly any[], varString: string | undefined): object; + // (undocumented) makeCacheKey(...args: any[]): object; // (undocumented) merge(older: string | StoreObject, newer: StoreObject | string): void; diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index f8c570ceebc..8edcfc86e4a 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -670,6 +670,12 @@ abstract class EntityStore implements NormalizedCache { // (undocumented) protected lookup(dataId: string, dependOnExistence?: boolean): StoreObject | undefined; // (undocumented) + makeCacheKey(document: DocumentNode, callback: Cache_2.WatchCallback, details: string): object; + // (undocumented) + makeCacheKey(selectionSet: SelectionSetNode, parent: string | StoreObject, varString: string | undefined, canonizeResults: boolean): object; + // (undocumented) + makeCacheKey(field: FieldNode, array: readonly any[], varString: string | undefined): object; + // (undocumented) makeCacheKey(...args: any[]): object; // (undocumented) merge(older: string | StoreObject, newer: StoreObject | string): void; diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 93fab5008fc..bd662573302 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -827,6 +827,12 @@ abstract class EntityStore implements NormalizedCache { // (undocumented) protected lookup(dataId: string, dependOnExistence?: boolean): StoreObject | undefined; // (undocumented) + makeCacheKey(document: DocumentNode, callback: Cache_2.WatchCallback, details: string): object; + // (undocumented) + makeCacheKey(selectionSet: SelectionSetNode, parent: string | StoreObject, varString: string | undefined, canonizeResults: boolean): object; + // (undocumented) + makeCacheKey(field: FieldNode, array: readonly any[], varString: string | undefined): object; + // (undocumented) makeCacheKey(...args: any[]): object; // (undocumented) merge(older: string | StoreObject, newer: StoreObject | string): void; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 71c954da766..20b14b5c8a5 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -831,6 +831,12 @@ abstract class EntityStore implements NormalizedCache { // (undocumented) protected lookup(dataId: string, dependOnExistence?: boolean): StoreObject | undefined; // (undocumented) + makeCacheKey(document: DocumentNode, callback: Cache_2.WatchCallback, details: string): object; + // (undocumented) + makeCacheKey(selectionSet: SelectionSetNode, parent: string | StoreObject, varString: string | undefined, canonizeResults: boolean): object; + // (undocumented) + makeCacheKey(field: FieldNode, array: readonly any[], varString: string | undefined): object; + // (undocumented) makeCacheKey(...args: any[]): object; // (undocumented) merge(older: string | StoreObject, newer: StoreObject | string): void; diff --git a/.changeset/thick-mice-collect.md b/.changeset/thick-mice-collect.md new file mode 100644 index 00000000000..47ed2e58cfd --- /dev/null +++ b/.changeset/thick-mice-collect.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Clarify types of `EntityStore.makeCacheKey`. diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts index a31f96db63a..4520d9740da 100644 --- a/src/cache/inmemory/entityStore.ts +++ b/src/cache/inmemory/entityStore.ts @@ -32,6 +32,7 @@ import type { DeleteModifier, ModifierDetails, } from "../core/types/common.js"; +import type { DocumentNode, FieldNode, SelectionSetNode } from "graphql"; const DELETE: DeleteModifier = Object.create(null); const delModifier: Modifier = () => DELETE; @@ -522,6 +523,27 @@ export abstract class EntityStore implements NormalizedCache { } // Used to compute cache keys specific to this.group. + /** overload for `InMemoryCache.maybeBroadcastWatch` */ + public makeCacheKey( + document: DocumentNode, + callback: Cache.WatchCallback, + details: string + ): object; + /** overload for `StoreReader.executeSelectionSet` */ + public makeCacheKey( + selectionSet: SelectionSetNode, + parent: string /* = ( Reference.__ref ) */ | StoreObject, + varString: string | undefined, + canonizeResults: boolean + ): object; + /** overload for `StoreReader.executeSubSelectedArray` */ + public makeCacheKey( + field: FieldNode, + array: readonly any[], + varString: string | undefined + ): object; + /** @deprecated This is only meant for internal usage, + * in your own code please use a `Trie` instance instead. */ public makeCacheKey(...args: any[]): object; public makeCacheKey() { return this.group.keyMaker.lookupArray(arguments); diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index 4ddbea78b43..35b9b1dce17 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -153,6 +153,10 @@ export class StoreReader { this.canon = config.canon || new ObjectCanon(); + // memoized functions in this class will be "garbage-collected" + // by recreating the whole `StoreReader` in + // `InMemoryCache.resetResultsCache` + // (triggered from `InMemoryCache.gc` with `resetResultCache: true`) this.executeSelectionSet = wrap( (options) => { const { canonizeResults } = options.context; From d9ca4f0821c66ae4f03cf35a7ac93fe604cc6de3 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 28 Nov 2023 22:37:24 +0100 Subject: [PATCH 31/90] Ensure `defaultContext` is also used for mutations and subscriptions (#11385) --- .api-reports/api-report-core.md | 6 +-- .api-reports/api-report-react.md | 6 +-- .api-reports/api-report-react_components.md | 6 +-- .api-reports/api-report-react_context.md | 6 +-- .api-reports/api-report-react_hoc.md | 6 +-- .api-reports/api-report-react_hooks.md | 6 +-- .api-reports/api-report-react_ssr.md | 6 +-- .api-reports/api-report-testing.md | 6 +-- .api-reports/api-report-testing_core.md | 6 +-- .api-reports/api-report-utilities.md | 6 +-- .api-reports/api-report.md | 6 +-- .changeset/quick-hats-marry.md | 5 ++ .size-limits.json | 4 +- src/core/QueryManager.ts | 6 +-- src/core/__tests__/QueryManager/index.ts | 56 ++++++++++++--------- 15 files changed, 74 insertions(+), 63 deletions(-) create mode 100644 .changeset/quick-hats-marry.md diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 8edcfc86e4a..4a8dfc429e8 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -2201,9 +2201,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:126:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:385:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:191:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 5d4ebef40d6..c915888d40c 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -2211,9 +2211,9 @@ interface WatchQueryOptions(it: (...args: TArgs // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:385:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:158:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:160:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 5aaef2383c6..313f20a2f54 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -1646,9 +1646,9 @@ export function withWarningSpy(it: (...args: TArgs // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:385:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:158:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:160:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index bd662573302..776f094979a 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -2534,9 +2534,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:385:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:158:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:160:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 20b14b5c8a5..0661f0423cd 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -2883,9 +2883,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:126:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:385:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:191:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:26:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts diff --git a/.changeset/quick-hats-marry.md b/.changeset/quick-hats-marry.md new file mode 100644 index 00000000000..2667f0a9750 --- /dev/null +++ b/.changeset/quick-hats-marry.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +ensure `defaultContext` is also used for mutations and subscriptions diff --git a/.size-limits.json b/.size-limits.json index 52108cf4fb9..76ca896a274 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38164, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32188 + "dist/apollo-client.min.cjs": 38160, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32186 } diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index e19cfdd71a3..98ab4d1d1f4 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -8,7 +8,6 @@ import { equal } from "@wry/equality"; import type { ApolloLink, FetchResult } from "../link/core/index.js"; import { execute } from "../link/core/index.js"; import { - compact, hasDirectives, isExecutionPatchIncrementalResult, isExecutionPatchResult, @@ -1161,9 +1160,7 @@ export class QueryManager { return asyncMap( this.getObservableFromLink( linkDocument, - // explicitly a shallow merge so any class instances etc. a user might - // put in here will not be merged into each other. - compact(this.defaultContext, options.context), + options.context, options.variables ), @@ -1676,6 +1673,7 @@ export class QueryManager { private prepareContext(context = {}) { const newContext = this.localState.prepareContext(context); return { + ...this.defaultContext, ...newContext, clientAwareness: this.clientAwareness, }; diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index be28a6b575e..6358d171f4c 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -6180,32 +6180,40 @@ describe("QueryManager", () => { }).toThrowError(/Cannot set property defaultContext/); }); - it("`defaultContext` will be applied to the context of a query", async () => { - let context: any; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new ApolloLink( - (operation) => - new Observable((observer) => { - ({ cache: _, ...context } = operation.getContext()); - observer.complete(); - }) - ), - defaultContext: { - foo: "bar", - }, - }); + it.each([ + ["query", { method: "query", option: "query" }], + ["mutation", { method: "mutate", option: "mutation" }], + ["subscription", { method: "subscribe", option: "query" }], + ] as const)( + "`defaultContext` will be applied to the context of a %s", + async (_, { method, option }) => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultContext: { + foo: "bar", + }, + }); - await client.query({ - query: gql` - query { - foo - } - `, - }); + // @ts-ignore a bit too generic for TS + client[method]({ + [option]: gql` + query { + foo + } + `, + }); - expect(context.foo).toBe("bar"); - }); + expect(context.foo).toBe("bar"); + } + ); it("`ApolloClient.defaultContext` can be modified and changes will show up in future queries", async () => { let context: any; From a8158733cfa3e65180ec23518d657ea41894bb2b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 28 Nov 2023 16:44:10 -0700 Subject: [PATCH 32/90] Add a new `useLoadableQuery` hook (#11300) --- .api-reports/api-report-react.md | 78 +- .api-reports/api-report-react_hooks.md | 82 +- .api-reports/api-report-utilities.md | 5 + .api-reports/api-report.md | 70 + .changeset/thirty-ties-arrive.md | 26 + .size-limit.cjs | 1 + .size-limits.json | 4 +- config/jest.config.js | 5 +- src/__tests__/__snapshots__/exports.ts.snap | 3 + .../hooks/__tests__/useLoadableQuery.test.tsx | 4719 +++++++++++++++++ src/react/hooks/index.ts | 5 + src/react/hooks/internal/index.ts | 1 + src/react/hooks/internal/useRenderGuard.ts | 22 + src/react/hooks/useLoadableQuery.ts | 230 + src/react/types/types.ts | 63 + .../disposables/disableActWarnings.ts | 15 + src/testing/internal/disposables/index.ts | 1 + src/testing/internal/profile/Render.tsx | 5 +- src/testing/internal/profile/context.tsx | 33 + src/testing/internal/profile/index.ts | 9 +- src/testing/internal/profile/profile.tsx | 224 +- src/testing/matchers/ProfiledComponent.ts | 33 +- src/testing/matchers/index.d.ts | 7 +- src/utilities/index.ts | 1 + src/utilities/types/OnlyRequiredProperties.ts | 6 + 25 files changed, 5553 insertions(+), 95 deletions(-) create mode 100644 .changeset/thirty-ties-arrive.md create mode 100644 src/react/hooks/__tests__/useLoadableQuery.test.tsx create mode 100644 src/react/hooks/internal/useRenderGuard.ts create mode 100644 src/react/hooks/useLoadableQuery.ts create mode 100644 src/testing/internal/disposables/disableActWarnings.ts create mode 100644 src/testing/internal/profile/context.tsx create mode 100644 src/utilities/types/OnlyRequiredProperties.ts diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index c915888d40c..9730ae3bc79 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -1018,6 +1018,38 @@ export type LazyQueryResultTuple = // @public (undocumented) type Listener = (promise: Promise>) => void; +// @public (undocumented) +export type LoadableQueryHookFetchPolicy = Extract; + +// @public (undocumented) +export interface LoadableQueryHookOptions { + // (undocumented) + canonizeResults?: boolean; + // (undocumented) + client?: ApolloClient; + // (undocumented) + context?: Context; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + errorPolicy?: ErrorPolicy; + // (undocumented) + fetchPolicy?: LoadableQueryHookFetchPolicy; + // (undocumented) + queryKey?: string | number | any[]; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + refetchWritePolicy?: RefetchWritePolicy; + // (undocumented) + returnPartialData?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type LoadQueryFunction = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; + // @public (undocumented) class LocalState { // Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts @@ -1119,8 +1151,6 @@ interface MutationBaseOptions { data: SubscriptionResult; } +// @public (undocumented) +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1800,6 +1835,9 @@ type RequestHandler = (operation: Operation, forward: NextLink) => Observable void; + // @public (undocumented) type Resolver = (rootValue?: any, args?: any, context?: any, info?: { field: FieldNode; @@ -2092,6 +2130,39 @@ export type UseFragmentResult = { // @public (undocumented) export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions & TOptions): UseLoadableQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult | undefined, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; +}): UseLoadableQueryResult, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; + +// @public (undocumented) +export type UseLoadableQueryResult = [ +LoadQueryFunction, +QueryReference | null, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; +} +]; + // @public (undocumented) export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; @@ -2193,8 +2264,6 @@ interface WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // // (undocumented) refetchWritePolicy?: RefetchWritePolicy; } @@ -2221,6 +2290,7 @@ interface WatchQueryOptions = [LazyQ // @public (undocumented) type Listener = (promise: Promise>) => void; +// @public (undocumented) +type LoadableQueryHookFetchPolicy = Extract; + +// @public (undocumented) +interface LoadableQueryHookOptions { + // (undocumented) + canonizeResults?: boolean; + // (undocumented) + client?: ApolloClient; + // (undocumented) + context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + errorPolicy?: ErrorPolicy; + // Warning: (ae-forgotten-export) The symbol "LoadableQueryHookFetchPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fetchPolicy?: LoadableQueryHookFetchPolicy; + // (undocumented) + queryKey?: string | number | any[]; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + refetchWritePolicy?: RefetchWritePolicy; + // (undocumented) + returnPartialData?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type LoadQueryFunction = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; + // @public (undocumented) class LocalState { // Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts @@ -1070,8 +1104,6 @@ interface MutationBaseOptions { data: SubscriptionResult; } +// @public (undocumented) +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1696,6 +1733,9 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; +// @public (undocumented) +type ResetFunction = () => void; + // @public (undocumented) type Resolver = (rootValue?: any, args?: any, context?: any, info?: { field: FieldNode; @@ -1979,6 +2019,41 @@ export type UseFragmentResult = { // @public (undocumented) export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; +// Warning: (ae-forgotten-export) The symbol "LoadableQueryHookOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions & TOptions): UseLoadableQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult | undefined, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; +}): UseLoadableQueryResult, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; + +// @public (undocumented) +export type UseLoadableQueryResult = [ +LoadQueryFunction, +QueryReference | null, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; +} +]; + // Warning: (ae-forgotten-export) The symbol "MutationHookOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationTuple" needs to be exported by the entry point index.d.ts // @@ -2087,8 +2162,6 @@ interface WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // // (undocumented) refetchWritePolicy?: RefetchWritePolicy; } @@ -2115,6 +2188,7 @@ interface WatchQueryOptions(keyArgs?: KeyArgs): FieldPo // @public (undocumented) export function omitDeep(value: T, key: K): DeepOmit; +// @public (undocumented) +export type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 0661f0423cd..ae6e27d72e9 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -1424,6 +1424,34 @@ export type LazyQueryResultTuple = // @public (undocumented) type Listener = (promise: Promise>) => void; +// @public (undocumented) +export type LoadableQueryHookFetchPolicy = Extract; + +// @public (undocumented) +export interface LoadableQueryHookOptions { + // (undocumented) + canonizeResults?: boolean; + // (undocumented) + client?: ApolloClient; + // (undocumented) + context?: DefaultContext; + // (undocumented) + errorPolicy?: ErrorPolicy; + // (undocumented) + fetchPolicy?: LoadableQueryHookFetchPolicy; + // (undocumented) + queryKey?: string | number | any[]; + // (undocumented) + refetchWritePolicy?: RefetchWritePolicy; + // (undocumented) + returnPartialData?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type LoadQueryFunction = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; + // @public (undocumented) class LocalState { // Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts @@ -1849,6 +1877,11 @@ export interface OnDataOptions { data: SubscriptionResult; } +// @public (undocumented) +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) export type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -2372,6 +2405,9 @@ export const resetApolloContext: typeof getApolloContext; export { resetCaches } +// @public (undocumented) +type ResetFunction = () => void; + // @public (undocumented) export type Resolver = (rootValue?: any, args?: any, context?: any, info?: { field: FieldNode; @@ -2740,6 +2776,39 @@ export type UseFragmentResult = { // @public (undocumented) export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions & TOptions): UseLoadableQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult | undefined, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; +}): UseLoadableQueryResult, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; + +// @public (undocumented) +export type UseLoadableQueryResult = [ +LoadQueryFunction, +QueryReference | null, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; +} +]; + // @public (undocumented) export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; @@ -2890,6 +2959,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:26:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:27:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useLoadableQuery.ts:51:5 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.changeset/thirty-ties-arrive.md b/.changeset/thirty-ties-arrive.md new file mode 100644 index 00000000000..c8a6fc22c86 --- /dev/null +++ b/.changeset/thirty-ties-arrive.md @@ -0,0 +1,26 @@ +--- +"@apollo/client": minor +--- + +Introduces a new `useLoadableQuery` hook. This hook works similarly to `useBackgroundQuery` in that it returns a `queryRef` that can be used to suspend a component via the `useReadQuery` hook. It provides a more ergonomic way to load the query during a user interaction (for example when wanting to preload some data) that would otherwise be clunky with `useBackgroundQuery`. + +```tsx +function App() { + const [loadQuery, queryRef, { refetch, fetchMore, reset }] = useLoadableQuery(query, options) + + return ( + <> + + }> + {queryRef && } + + + ); +} + +function Child({ queryRef }) { + const { data } = useReadQuery(queryRef) + + // ... +} +``` diff --git a/.size-limit.cjs b/.size-limit.cjs index b6edc78d7bb..7c7b71da42f 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -20,6 +20,7 @@ const checks = [ "useSubscription", "useSuspenseQuery", "useBackgroundQuery", + "useLoadableQuery", "useReadQuery", "useFragment", ].map((name) => ({ path: "dist/react/index.js", import: `{ ${name} }` })), diff --git a/.size-limits.json b/.size-limits.json index 76ca896a274..7bc50667da7 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38160, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32186 + "dist/apollo-client.min.cjs": 38600, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32187 } diff --git a/config/jest.config.js b/config/jest.config.js index 3dcd6e6de56..a45df96fc48 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -31,10 +31,11 @@ const ignoreTSXFiles = ".tsx$"; const react17TestFileIgnoreList = [ ignoreTSFiles, - // For now, we only support useSuspenseQuery with React 18, so no need to test - // it with React 17 + // We only support Suspense with React 18, so don't test suspense hooks with + // React 17 "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", + "src/react/hooks/__tests__/useLoadableQuery.test.tsx", ]; const tsStandardConfig = { diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 1d9a73e0eb7..70229c88a17 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -59,6 +59,7 @@ Array [ "useBackgroundQuery", "useFragment", "useLazyQuery", + "useLoadableQuery", "useMutation", "useQuery", "useReactiveVar", @@ -273,6 +274,7 @@ Array [ "useBackgroundQuery", "useFragment", "useLazyQuery", + "useLoadableQuery", "useMutation", "useQuery", "useReactiveVar", @@ -316,6 +318,7 @@ Array [ "useBackgroundQuery", "useFragment", "useLazyQuery", + "useLoadableQuery", "useMutation", "useQuery", "useReactiveVar", diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx new file mode 100644 index 00000000000..e82dc181f66 --- /dev/null +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -0,0 +1,4719 @@ +import React, { Suspense, useState } from "react"; +import { + act, + render, + screen, + renderHook, + waitFor, + RenderOptions, +} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary"; +import { expectTypeOf } from "expect-type"; +import { GraphQLError } from "graphql"; +import { + gql, + ApolloError, + ApolloClient, + ErrorPolicy, + NetworkStatus, + TypedDocumentNode, + ApolloLink, + Observable, + OperationVariables, + RefetchWritePolicy, +} from "../../../core"; +import { + MockedProvider, + MockedProviderProps, + MockedResponse, + MockLink, + MockSubscriptionLink, + wait, +} from "../../../testing"; +import { + concatPagination, + offsetLimitPagination, + DeepPartial, +} from "../../../utilities"; +import { useLoadableQuery } from "../useLoadableQuery"; +import type { UseReadQueryResult } from "../useReadQuery"; +import { useReadQuery } from "../useReadQuery"; +import { ApolloProvider } from "../../context"; +import { InMemoryCache } from "../../../cache"; +import { LoadableQueryHookFetchPolicy } from "../../types/types"; +import { QueryReference } from "../../../react"; +import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; +import invariant, { InvariantError } from "ts-invariant"; +import { + Profiler, + createProfiler, + spyOnConsole, + useTrackRenders, +} from "../../../testing/internal"; + +interface SimpleQueryData { + greeting: string; +} + +function useSimpleQueryCase() { + const query: TypedDocumentNode = gql` + query GreetingQuery { + greeting + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + delay: 10, + }, + ]; + + return { query, mocks }; +} + +interface VariablesCaseData { + character: { + id: string; + name: string; + }; +} + +interface VariablesCaseVariables { + id: string; +} + +function useVariablesQueryCase() { + const query: TypedDocumentNode< + VariablesCaseData, + VariablesCaseVariables + > = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"]; + + const mocks: MockedResponse[] = [...CHARACTERS].map( + (name, index) => ({ + request: { query, variables: { id: String(index + 1) } }, + result: { data: { character: { id: String(index + 1), name } } }, + delay: 20, + }) + ); + + return { mocks, query }; +} + +interface PaginatedQueryData { + letters: { + letter: string; + position: number; + }[]; +} + +interface PaginatedQueryVariables { + limit?: number; + offset?: number; +} + +function usePaginatedQueryCase() { + const query: TypedDocumentNode< + PaginatedQueryData, + PaginatedQueryVariables + > = gql` + query letters($limit: Int, $offset: Int) { + letters(limit: $limit) { + letter + position + } + } + `; + + const data = "ABCDEFG" + .split("") + .map((letter, index) => ({ letter, position: index + 1 })); + + const link = new ApolloLink((operation) => { + const { offset = 0, limit = 2 } = operation.variables; + const letters = data.slice(offset, offset + limit); + + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { letters } }); + observer.complete(); + }, 10); + }); + }); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + return { query, link, client }; +} + +function createDefaultProfiler() { + return createProfiler({ + initialSnapshot: { + error: null as Error | null, + result: null as UseReadQueryResult | null, + }, + }); +} + +function createDefaultProfiledComponents< + Snapshot extends { + result: UseReadQueryResult | null; + error?: Error | null; + }, + TData = Snapshot["result"] extends UseReadQueryResult | null + ? TData + : unknown, +>(profiler: Profiler) { + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook({ queryRef }: { queryRef: QueryReference }) { + useTrackRenders(); + profiler.mergeSnapshot({ + result: useReadQuery(queryRef), + } as Partial); + + return null; + } + + function ErrorFallback({ error }: { error: Error }) { + useTrackRenders(); + profiler.mergeSnapshot({ error } as Partial); + + return
Oops
; + } + + function ErrorBoundary({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + return { + SuspenseFallback, + ReadQueryHook, + ErrorFallback, + ErrorBoundary, + }; +} + +function renderWithMocks( + ui: React.ReactElement, + { + wrapper: Wrapper = React.Fragment, + ...props + }: MockedProviderProps & { wrapper?: RenderOptions["wrapper"] } +) { + const user = userEvent.setup(); + + const utils = render(ui, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + return { ...utils, user }; +} + +function renderWithClient( + ui: React.ReactElement, + options: { client: ApolloClient; wrapper?: RenderOptions["wrapper"] } +) { + const { client, wrapper: Wrapper = React.Fragment } = options; + const user = userEvent.setup(); + + const utils = render(ui, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + return { ...utils, user }; +} + +it("loads a query and suspends when the load query function is called", async () => { + const { query, mocks } = useSimpleQueryCase(); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + } +}); + +it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { + const { query, mocks } = useVariablesQueryCase(); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("changes variables on a query and resuspends when passing new variables to the loadQuery function", async () => { + const { query, mocks } = useVariablesQueryCase(); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const App = () => { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + }; + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } + + await act(() => user.click(screen.getByText("Load 1st character"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await act(() => user.click(screen.getByText("Load 2nd character"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "2", name: "Black Widow" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("resets the `queryRef` to null and disposes of it when calling the `reset` function", async () => { + const { query, mocks } = useSimpleQueryCase(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { reset }] = useLoadableQuery(query); + + // Resetting the result allows us to detect when ReadQueryHook is unmounted + // since it won't render and overwrite the `null` + Profiler.mergeSnapshot({ result: null }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Reset query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toBeNull(); + } + + // Since dispose is called in a setTimeout, we need to wait a tick before + // checking to see if the query ref was properly disposed + await wait(0); + + expect(client.getObservableQueries().size).toBe(0); +}); + +it("allows the client to be overridden", async () => { + const { query } = useSimpleQueryCase(); + + const globalClient = new ApolloClient({ + link: new ApolloLink(() => + Observable.of({ data: { greeting: "global hello" } }) + ), + cache: new InMemoryCache(), + }); + + const localClient = new ApolloClient({ + link: new ApolloLink(() => + Observable.of({ data: { greeting: "local hello" } }) + ), + cache: new InMemoryCache(), + }); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [loadQuery, queryRef] = useLoadableQuery(query, { + client: localClient, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { + client: globalClient, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "local hello" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); +}); + +it("passes context to the link", async () => { + interface QueryData { + context: Record; + } + + const query: TypedDocumentNode = gql` + query ContextQuery { + context + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink((operation) => { + return new Observable((observer) => { + const { valueA, valueB } = operation.getContext(); + + observer.next({ data: { context: { valueA, valueB } } }); + observer.complete(); + }); + }), + }); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [loadQuery, queryRef] = useLoadableQuery(query, { + context: { valueA: "A", valueB: "B" }, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { context: { valueA: "A", valueB: "B" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); +}); + +it('enables canonical results when canonizeResults is "true"', async () => { + interface Result { + __typename: string; + value: number; + } + + interface QueryData { + results: Result[]; + } + + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); + + const query: TypedDocumentNode = gql` + query { + results { + value + } + } + `; + + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; + + cache.writeQuery({ + query, + data: { results }, + }); + + const client = new ApolloClient({ + cache, + link: new MockLink([]), + }); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [loadQuery, queryRef] = useLoadableQuery(query, { + canonizeResults: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + const { snapshot } = await Profiler.takeRender(); + const resultSet = new Set(snapshot.result?.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(snapshot.result).toEqual({ + data: { results }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); +}); + +it("can disable canonical results when the cache's canonizeResults setting is true", async () => { + interface Result { + __typename: string; + value: number; + } + + interface QueryData { + results: Result[]; + } + + const cache = new InMemoryCache({ + canonizeResults: true, + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); + + const query: TypedDocumentNode<{ results: Result[] }, never> = gql` + query { + results { + value + } + } + `; + + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; + + cache.writeQuery({ + query, + data: { results }, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [loadQuery, queryRef] = useLoadableQuery(query, { + canonizeResults: false, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { cache, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + const { snapshot } = await Profiler.takeRender(); + const resultSet = new Set(snapshot.result!.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(snapshot.result).toEqual({ + data: { results }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); +}); + +it("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { + type QueryData = { hello: string }; + const query: TypedDocumentNode = gql` + query { + hello + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ link, cache }); + + cache.writeQuery({ query, data: { hello: "from cache" } }); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-and-network", + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { hello: "from cache" }, + networkStatus: NetworkStatus.loading, + error: undefined, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } +}); + +it("all data is present in the cache, no network request is made", async () => { + const query = gql` + query { + hello + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ + link, + cache, + }); + + cache.writeQuery({ query, data: { hello: "from cache" } }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { hello: "from cache" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + + await expect(Profiler).not.toRerender(); +}); + +it("partial data is present in the cache so it is ignored and network request is made", async () => { + const query = gql` + { + hello + foo + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link", foo: "bar" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ + link, + cache, + }); + + { + // we expect a "Missing field 'foo' while writing result..." error + // when writing hello to the cache, so we'll silence the console.error + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ query, data: { hello: "from cache" } }); + } + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { foo: "bar", hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", async () => { + const query = gql` + query { + hello + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ + link, + cache, + }); + + cache.writeQuery({ query, data: { hello: "from cache" } }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "network-only", + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it("fetches data from the network but does not update the cache when `fetchPolicy` is 'no-cache'", async () => { + const query = gql` + query { + hello + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ link, cache }); + + cache.writeQuery({ query, data: { hello: "from cache" } }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "no-cache", + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(client.extract()).toEqual({ + ROOT_QUERY: { __typename: "Query", hello: "from cache" }, + }); +}); + +it("works with startTransition to change variables", async () => { + type Variables = { + id: string; + }; + + interface Data { + todo: { + id: string; + name: string; + completed: boolean; + }; + } + + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed + } + } + `; + + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + todo: { id: "2", name: "Take out trash", completed: true }, + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + function SuspenseFallback() { + return

Loading

; + } + + function App() { + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( +
+ + }> + {queryRef && ( + loadQuery({ id })} /> + )} + +
+ ); + } + + function Todo({ + queryRef, + onChange, + }: { + queryRef: QueryReference; + onChange: (id: string) => void; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todo } = data; + + return ( + <> + +
+ {todo.name} + {todo.completed && " (completed)"} +
+ + ); + } + + const { user } = renderWithClient(, { client }); + + await act(() => user.click(screen.getByText("Load first todo"))); + + expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(await screen.findByTestId("todo")).toBeInTheDocument(); + + const todo = screen.getByTestId("todo"); + const button = screen.getByText("Refresh"); + + expect(todo).toHaveTextContent("Clean room"); + + await act(() => user.click(button)); + + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + + // We can ensure this works with isPending from useTransition in the process + expect(todo).toHaveAttribute("aria-busy", "true"); + + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); + + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Take out trash (completed)"); + }); +}); + +it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } + + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + const client = new ApolloClient({ cache, link }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-and-network", + }); + return ( +
+ + }> + {queryRef && } + +
+ ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load todo"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + link.simulateResult({ + result: { + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + hasNext: true, + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("reacts to cache updates", async () => { + const { query, mocks } = useSimpleQueryCase(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ + query, + data: { greeting: "Updated Hello" }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Updated Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("applies `errorPolicy` on next fetch when it changes between renders", async () => { + const { query } = useSimpleQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + }, + { + request: { query }, + result: { + errors: [new GraphQLError("oops")], + }, + }, + ]; + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [errorPolicy, setErrorPolicy] = useState("none"); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + errorPolicy, + }); + + return ( + <> + + + + }> + + {queryRef && } + + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Change error policy"))); + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Refetch greeting"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + // Ensure we aren't rendering the error boundary and instead rendering the + // error message in the hook component. + expect(renderedComponents).not.toContain(ErrorFallback); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), + networkStatus: NetworkStatus.error, + }); + } +}); + +it("applies `context` on next fetch when it changes between renders", async () => { + interface Data { + phase: string; + } + + const query: TypedDocumentNode = gql` + query { + phase + } + `; + + const link = new ApolloLink((operation) => { + return Observable.of({ + data: { + phase: operation.getContext().phase, + }, + }); + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [phase, setPhase] = React.useState("initial"); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + context: { phase }, + }); + + return ( + <> + + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result!.data).toEqual({ + phase: "initial", + }); + } + + await act(() => user.click(screen.getByText("Update context"))); + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result!.data).toEqual({ + phase: "rerender", + }); + } +}); + +// NOTE: We only test the `false` -> `true` path here. If the option changes +// from `true` -> `false`, the data has already been canonized, so it has no +// effect on the output. +it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { + interface Result { + __typename: string; + value: number; + } + + interface Data { + results: Result[]; + } + + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); + + const query: TypedDocumentNode = gql` + query { + results { + value + } + } + `; + + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; + + cache.writeQuery({ + query, + data: { results }, + }); + + const client = new ApolloClient({ + link: new MockLink([]), + cache, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [canonizeResults, setCanonizeResults] = React.useState(false); + const [loadQuery, queryRef] = useLoadableQuery(query, { + canonizeResults, + }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await Profiler.takeRender(); + const { data } = snapshot.result!; + const resultSet = new Set(data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(data.results.length).toBe(6); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); + } + + await act(() => user.click(screen.getByText("Canonize results"))); + + { + const { snapshot } = await Profiler.takeRender(); + const { data } = snapshot.result!; + const resultSet = new Set(data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(data.results.length).toBe(6); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); + } +}); + +it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { + interface Data { + primes: number[]; + } + + const query: TypedDocumentNode = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + { + request: { query, variables: { min: 30, max: 50 } }, + result: { data: { primes: [31, 37, 41, 43, 47] } }, + delay: 10, + }, + ]; + + const mergeParams: [number[] | undefined, number[]][] = []; + + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, + }, + }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [refetchWritePolicy, setRefetchWritePolicy] = + React.useState("merge"); + + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + refetchWritePolicy, + }); + + return ( + <> + + + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + const { primes } = snapshot.result!.data; + + expect(primes).toEqual([2, 3, 5, 7, 11]); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } + + await act(() => user.click(screen.getByText("Refetch next"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + const { primes } = snapshot.result!.data; + + expect(primes).toEqual([2, 3, 5, 7, 11, 13, 17, 19, 23, 29]); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + ]); + } + + await act(() => user.click(screen.getByText("Change refetch write policy"))); + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Refetch last"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + const { primes } = snapshot.result!.data; + + expect(primes).toEqual([31, 37, 41, 43, 47]); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + [undefined, [31, 37, 41, 43, 47]], + ]); + } +}); + +it("applies `returnPartialData` on next fetch when it changes between renders", async () => { + interface Data { + character: { + __typename: "Character"; + id: string; + name: string; + }; + } + + interface PartialData { + character: { + __typename: "Character"; + id: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + __typename + id + name + } + } + `; + + const partialQuery: TypedDocumentNode = gql` + query { + character { + __typename + id + } + } + `; + + const mocks = [ + { + request: { query: fullQuery }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", + }, + }, + }, + }, + { + request: { query: fullQuery }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange (refetched)", + }, + }, + }, + delay: 100, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [returnPartialData, setReturnPartialData] = React.useState(false); + + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { + returnPartialData, + }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Doctor Strange" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Update partial data"))); + await Profiler.takeRender(); + + cache.modify({ + id: cache.identify({ __typename: "Character", id: "1" }), + fields: { + name: (_, { DELETE }) => DELETE, + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1" }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange (refetched)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { + interface Data { + character: { + __typename: "Character"; + id: string; + name: string; + }; + } + + const query: TypedDocumentNode = gql` + query { + character { + __typename + id + name + } + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", + }, + }, + }, + delay: 10, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query, + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [fetchPolicy, setFetchPolicy] = + React.useState("cache-first"); + + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + fetchPolicy, + }); + + return ( + <> + + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Change fetch policy"))); + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // Because we switched to a `no-cache` fetch policy, we should not see the + // newly fetched data in the cache after the fetch occured. + expect(cache.readQuery({ query })).toEqual({ + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }); +}); + +it("re-suspends when calling `refetch`", async () => { + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Spider-Man" } }, + }, + delay: 20, + }, + // refetch + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Spider-Man (updated)" } }, + }, + delay: 20, + }, + ]; + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man (updated)" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("re-suspends when calling `refetch` with new variables", async () => { + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { character: { id: "2", name: "Captain America" } }, + }, + }, + ]; + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch with ID 2"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "2", name: "Captain America" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("re-suspends multiple times when calling `refetch` multiple times", async () => { + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Spider-Man" } }, + }, + maxUsageCount: 3, + }, + ]; + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + const button = screen.getByText("Refetch"); + + await act(() => user.click(button)); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(button)); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("throws errors when errors are returned after calling `refetch`", async () => { + using _consoleSpy = spyOnConsole("error"); + + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + delay: 20, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + delay: 20, + }, + ]; + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + + }> + + {queryRef && } + + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(snapshot.error).toEqual( + new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }) + ); + } +}); + +it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { + const { query } = useVariablesQueryCase(); + + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + }, + ]; + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + errorPolicy: "ignore", + }); + + return ( + <> + + + }> + + {queryRef && } + + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toStrictEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.error).toBeNull(); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + expect(renderedComponents).not.toContain(ErrorFallback); + } + + await expect(Profiler).not.toRerender(); +}); + +it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + delay: 20, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + delay: 20, + }, + ]; + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + errorPolicy: "all", + }); + + return ( + <> + + + }> + + {queryRef && } + + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).not.toContain(ErrorFallback); + expect(snapshot.error).toBeNull(); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { + const { query } = useVariablesQueryCase(); + + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + delay: 20, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: null } }, + errors: [new GraphQLError("Something went wrong")], + }, + delay: 20, + }, + ]; + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + errorPolicy: "all", + }); + + return ( + <> + + + }> + + {queryRef && } + + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).not.toContain(ErrorFallback); + expect(snapshot.error).toBeNull(); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: null } }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + id: string; + }; + + interface Data { + todo: { + id: string; + name: string; + completed: boolean; + }; + } + + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + }, + delay: 10, + }, + ]; + + function SuspenseFallback() { + return

Loading

; + } + + function App() { + const [id, setId] = React.useState("1"); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && ( + + )} + + + ); + } + + function Todo({ + queryRef, + refetch, + }: { + refetch: RefetchFunction; + queryRef: QueryReference; + onChange: (id: string) => void; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todo } = data; + + return ( + <> + +
+ {todo.name} + {todo.completed && " (completed)"} +
+ + ); + } + + const { user } = renderWithMocks(, { mocks }); + + await act(() => user.click(screen.getByText("Load query"))); + + expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(await screen.findByTestId("todo")).toBeInTheDocument(); + + const todo = screen.getByTestId("todo"); + const button = screen.getByText("Refresh"); + + expect(todo).toHaveTextContent("Clean room"); + + await act(() => user.click(button)); + + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + + // We can ensure this works with isPending from useTransition in the process + expect(todo).toHaveAttribute("aria-busy", "true"); + + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); + + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Clean room (completed)"); + }); +}); + +it("re-suspends when calling `fetchMore` with different variables", async () => { + const { query, client } = usePaginatedQueryCase(); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Fetch more"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("properly uses `updateQuery` when calling `fetchMore`", async () => { + const { query, client } = usePaginatedQueryCase(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Fetch more"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + const { query, link } = usePaginatedQueryCase(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + letters: concatPagination(), + }, + }, + }, + }), + }); + + function App() { + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Fetch more"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + offset: number; + }; + + interface Todo { + __typename: "Todo"; + id: string; + name: string; + completed: boolean; + } + interface Data { + todos: Todo[]; + } + + const query: TypedDocumentNode = gql` + query TodosQuery($offset: Int!) { + todos(offset: $offset) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { offset: 0 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + }, + delay: 10, + }, + { + request: { query, variables: { offset: 1 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + todos: offsetLimitPagination(), + }, + }, + }, + }), + }); + + function SuspenseFallback() { + return

Loading

; + } + + function App() { + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Todo({ + queryRef, + fetchMore, + }: { + fetchMore: FetchMoreFunction; + queryRef: QueryReference; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todos } = data; + + return ( + <> + +
+ {todos.map((todo) => ( +
+ {todo.name} + {todo.completed && " (completed)"} +
+ ))} +
+ + ); + } + + const { user } = renderWithClient(, { client }); + + await act(() => user.click(screen.getByText("Load query"))); + + expect(screen.getByText("Loading")).toBeInTheDocument(); + + expect(await screen.findByTestId("todos")).toBeInTheDocument(); + + const todos = screen.getByTestId("todos"); + const todo1 = screen.getByTestId("todo:1"); + const button = screen.getByText("Load more"); + + expect(todo1).toBeInTheDocument(); + + await act(() => user.click(button)); + + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + + // We can ensure this works with isPending from useTransition in the process + expect(todos).toHaveAttribute("aria-busy", "true"); + + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo1).toHaveTextContent("Clean room"); + + // Eventually we should see the updated todos content once its done + // suspending. + await waitFor(() => { + expect(screen.getByTestId("todo:2")).toHaveTextContent( + "Take out trash (completed)" + ); + expect(todo1).toHaveTextContent("Clean room"); + }); +}); + +it('honors refetchWritePolicy set to "merge"', async () => { + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + interface QueryData { + primes: number[]; + } + + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + ]; + + const mergeParams: [number[] | undefined, number[]][] = []; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, + }, + }, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function App() { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + refetchWritePolicy: "merge", + }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + ]); + } + + await expect(Profiler).not.toRerender(); +}); + +it('defaults refetchWritePolicy to "overwrite"', async () => { + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + interface QueryData { + primes: number[]; + } + + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + ]; + + const mergeParams: [number[] | undefined, number[]][] = []; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, + }, + }, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function App() { + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial load + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [undefined, [13, 17, 19, 23, 29]], + ]); + } +}); + +it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { + interface Data { + character: { + id: string; + name: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + delay: 20, + }, + ]; + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + const client = new ApolloClient({ link: new MockLink(mocks), cache }); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial load + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + } + + await expect(Profiler).not.toRerender(); +}); + +it('suspends and does not use partial data from other variables in the cache when changing variables and using a "cache-first" fetch policy with returnPartialData: true', async () => { + const { query, mocks } = useVariablesQueryCase(); + + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id + } + } + `; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + variables: { id: "1" }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + } + + await act(() => user.click(screen.getByText("Change variables"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "2", name: "Black Widow" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + } + + await expect(Profiler).not.toRerender(); +}); + +it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { + interface Data { + character: { + id: string; + name: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + }, + ]; + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { + fetchPolicy: "network-only", + returnPartialData: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { + using _consoleSpy = spyOnConsole("warn"); + + interface Data { + character: { + id: string; + name: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { + fetchPolicy: "no-cache", + returnPartialData: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: Profiler, + }); + + // initial load + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { + using _consoleSpy = spyOnConsole("warn"); + + const query: TypedDocumentNode = gql` + query UserQuery { + greeting + } + `; + + renderHook( + () => + useLoadableQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy." + ); +}); + +it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { + interface Data { + character: { + id: string; + name: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + delay: 20, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { + fetchPolicy: "cache-and-network", + returnPartialData: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { + const { query, mocks } = useVariablesQueryCase(); + + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id + } + } + `; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + variables: { id: "1" }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-and-network", + returnPartialData: true, + }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Change variables"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "2", name: "Black Widow" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + interface QueryData { + greeting: { + __typename: string; + message?: string; + recipient?: { + __typename: string; + name: string; + }; + }; + } + + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + + { + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + using _consoleSpy = spyOnConsole("error"); + + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } + + const client = new ApolloClient({ link, cache }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadTodo, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + + return ( +
+ + }> + {queryRef && } + +
+ ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load todo"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + link.simulateResult({ + result: { + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + hasNext: true, + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("throws when calling loadQuery on first render", async () => { + using _consoleSpy = spyOnConsole("error"); + const { query, mocks } = useSimpleQueryCase(); + + function App() { + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + + return null; + } + + expect(() => renderWithMocks(, { mocks })).toThrow( + new InvariantError( + "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." + ) + ); +}); + +it("throws when calling loadQuery on subsequent render", async () => { + using _consoleSpy = spyOnConsole("error"); + const { query, mocks } = useSimpleQueryCase(); + + let error!: Error; + + function App() { + const [count, setCount] = useState(0); + const [loadQuery] = useLoadableQuery(query); + + if (count === 1) { + loadQuery(); + } + + return ; + } + + const { user } = renderWithMocks( + (error = e)} fallback={
Oops
}> + +
, + { mocks } + ); + + await act(() => user.click(screen.getByText("Load query in render"))); + + expect(error).toEqual( + new InvariantError( + "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." + ) + ); +}); + +it("allows loadQuery to be called in useEffect on first render", async () => { + const { query, mocks } = useSimpleQueryCase(); + + function App() { + const [loadQuery] = useLoadableQuery(query); + + React.useEffect(() => { + loadQuery(); + }, []); + + return null; + } + + expect(() => renderWithMocks(, { mocks })).not.toThrow(); +}); + +describe.skip("type tests", () => { + it("returns unknown when TData cannot be inferred", () => { + const query = gql``; + + const [, queryRef] = useLoadableQuery(query); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + }); + + it("variables are optional and can be anything with an untyped DocumentNode", () => { + const query = gql``; + + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + loadQuery({}); + loadQuery({ foo: "bar" }); + loadQuery({ bar: "baz" }); + }); + + it("variables are optional and can be anything with unspecified TVariables on a TypedDocumentNode", () => { + const query: TypedDocumentNode<{ greeting: string }> = gql``; + + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + loadQuery({}); + loadQuery({ foo: "bar" }); + loadQuery({ bar: "baz" }); + }); + + it("variables are optional when TVariables are empty", () => { + const query: TypedDocumentNode< + { greeting: string }, + Record + > = gql``; + + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + loadQuery({}); + // @ts-expect-error unknown variable + loadQuery({ foo: "bar" }); + }); + + it("does not allow variables when TVariables is `never`", () => { + const query: TypedDocumentNode<{ greeting: string }, never> = gql``; + + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + // @ts-expect-error no variables argument allowed + loadQuery({}); + // @ts-expect-error no variables argument allowed + loadQuery({ foo: "bar" }); + }); + + it("optional variables are optional to loadQuery", () => { + const query: TypedDocumentNode< + { posts: string[] }, + { limit?: number } + > = gql``; + + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + loadQuery({}); + loadQuery({ limit: 10 }); + loadQuery({ + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }); + }); + + it("enforces required variables when TVariables includes required variables", () => { + const query: TypedDocumentNode< + { character: string }, + { id: string } + > = gql``; + + const [loadQuery] = useLoadableQuery(query); + + // @ts-expect-error missing variables argument + loadQuery(); + // @ts-expect-error empty variables + loadQuery({}); + loadQuery({ id: "1" }); + loadQuery({ + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }); + }); + + it("requires variables with mixed TVariables", () => { + const query: TypedDocumentNode< + { character: string }, + { id: string; language?: string } + > = gql``; + + const [loadQuery] = useLoadableQuery(query); + + // @ts-expect-error missing variables argument + loadQuery(); + // @ts-expect-error empty variables + loadQuery({}); + loadQuery({ id: "1" }); + // @ts-expect-error missing required variable + loadQuery({ language: "en" }); + loadQuery({ id: "1", language: "en" }); + loadQuery({ + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }); + }); + + it("returns TData in default case", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it('returns TData | undefined with errorPolicy: "ignore"', () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + errorPolicy: "ignore", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "ignore" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it('returns TData | undefined with errorPolicy: "all"', () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + errorPolicy: "all", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "all" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it('returns TData with errorPolicy: "none"', () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + errorPolicy: "none", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "none" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it("returns DeepPartial with returnPartialData: true", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + returnPartialData: true, + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + }); + + it("returns TData with returnPartialData: false", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + returnPartialData: false, + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: false }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it("returns TData when passing an option that does not affect TData", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + fetchPolicy: "no-cache", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { fetchPolicy: "no-cache" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it("handles combinations of options", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: "ignore" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + } + + { + const [, queryRef] = useLoadableQuery(query, { + returnPartialData: true, + errorPolicy: "none", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: "none" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + }); + + it("returns correct TData type when combined options that do not affect TData", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + }); +}); diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 61d50665cac..8a725261f40 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -11,6 +11,11 @@ export type { UseSuspenseQueryResult } from "./useSuspenseQuery.js"; export { useSuspenseQuery } from "./useSuspenseQuery.js"; export type { UseBackgroundQueryResult } from "./useBackgroundQuery.js"; export { useBackgroundQuery } from "./useBackgroundQuery.js"; +export type { + LoadQueryFunction, + UseLoadableQueryResult, +} from "./useLoadableQuery.js"; +export { useLoadableQuery } from "./useLoadableQuery.js"; export type { UseReadQueryResult } from "./useReadQuery.js"; export { useReadQuery } from "./useReadQuery.js"; export { skipToken } from "./constants.js"; diff --git a/src/react/hooks/internal/index.ts b/src/react/hooks/internal/index.ts index d1c90c41f4a..2a45719986b 100644 --- a/src/react/hooks/internal/index.ts +++ b/src/react/hooks/internal/index.ts @@ -1,4 +1,5 @@ // These hooks are used internally and are not exported publicly by the library export { useDeepMemo } from "./useDeepMemo.js"; export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js"; +export { useRenderGuard } from "./useRenderGuard.js"; export { __use } from "./__use.js"; diff --git a/src/react/hooks/internal/useRenderGuard.ts b/src/react/hooks/internal/useRenderGuard.ts new file mode 100644 index 00000000000..98bb21a8ef1 --- /dev/null +++ b/src/react/hooks/internal/useRenderGuard.ts @@ -0,0 +1,22 @@ +import * as React from "rehackt"; + +function getRenderDispatcher() { + return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED + ?.ReactCurrentDispatcher?.current; +} + +let RenderDispatcher: unknown = null; + +/* +Relay does this too, so we hope this is safe. +https://github.com/facebook/relay/blob/8651fbca19adbfbb79af7a3bc40834d105fd7747/packages/react-relay/relay-hooks/loadQuery.js#L90-L98 +*/ +export function useRenderGuard() { + RenderDispatcher = getRenderDispatcher(); + + return React.useCallback(() => { + return ( + RenderDispatcher !== null && RenderDispatcher === getRenderDispatcher() + ); + }, []); +} diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts new file mode 100644 index 00000000000..2ec551bf26e --- /dev/null +++ b/src/react/hooks/useLoadableQuery.ts @@ -0,0 +1,230 @@ +import * as React from "rehackt"; +import type { + DocumentNode, + FetchMoreQueryOptions, + OperationVariables, + TypedDocumentNode, + WatchQueryOptions, +} from "../../core/index.js"; +import { useApolloClient } from "./useApolloClient.js"; +import { wrapQueryRef } from "../cache/QueryReference.js"; +import type { + QueryReference, + InternalQueryReference, +} from "../cache/QueryReference.js"; +import type { LoadableQueryHookOptions } from "../types/types.js"; +import { __use, useRenderGuard } from "./internal/index.js"; +import { getSuspenseCache } from "../cache/index.js"; +import { useWatchQueryOptions } from "./useSuspenseQuery.js"; +import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; +import { canonicalStringify } from "../../cache/index.js"; +import type { + DeepPartial, + OnlyRequiredProperties, +} from "../../utilities/index.js"; +import type { CacheKey } from "../cache/types.js"; +import { invariant } from "../../utilities/globals/index.js"; + +export type LoadQueryFunction = ( + // Use variadic args to handle cases where TVariables is type `never`, in + // which case we don't want to allow a variables argument. In other + // words, we don't want to allow variables to be passed as an argument to this + // function if the query does not expect variables in the document. + ...args: [TVariables] extends [never] + ? [] + : {} extends OnlyRequiredProperties + ? [variables?: TVariables] + : [variables: TVariables] +) => void; + +type ResetFunction = () => void; + +export type UseLoadableQueryResult< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> = [ + LoadQueryFunction, + QueryReference | null, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; + }, +]; + +export function useLoadableQuery< + TData, + TVariables extends OperationVariables, + TOptions extends LoadableQueryHookOptions, +>( + query: DocumentNode | TypedDocumentNode, + options?: LoadableQueryHookOptions & TOptions +): UseLoadableQueryResult< + TOptions["errorPolicy"] extends "ignore" | "all" + ? TOptions["returnPartialData"] extends true + ? DeepPartial | undefined + : TData | undefined + : TOptions["returnPartialData"] extends true + ? DeepPartial + : TData, + TVariables +>; + +export function useLoadableQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + } +): UseLoadableQueryResult | undefined, TVariables>; + +export function useLoadableQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; + } +): UseLoadableQueryResult; + +export function useLoadableQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: LoadableQueryHookOptions & { + returnPartialData: true; + } +): UseLoadableQueryResult, TVariables>; + +export function useLoadableQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options?: LoadableQueryHookOptions +): UseLoadableQueryResult; + +export function useLoadableQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: LoadableQueryHookOptions = Object.create(null) +): UseLoadableQueryResult { + const client = useApolloClient(options.client); + const suspenseCache = getSuspenseCache(client); + const watchQueryOptions = useWatchQueryOptions({ client, query, options }); + const { queryKey = [] } = options; + + const [queryRef, setQueryRef] = + React.useState | null>(null); + + const [promiseCache, setPromiseCache] = React.useState(() => + queryRef ? new Map([[queryRef.key, queryRef.promise]]) : new Map() + ); + + if (queryRef?.didChangeOptions(watchQueryOptions)) { + const promise = queryRef.applyOptions(watchQueryOptions); + promiseCache.set(queryRef.key, promise); + } + + if (queryRef) { + queryRef.promiseCache = promiseCache; + } + + const calledDuringRender = useRenderGuard(); + + React.useEffect(() => queryRef?.retain(), [queryRef]); + + const fetchMore: FetchMoreFunction = React.useCallback( + (options) => { + if (!queryRef) { + throw new Error( + "The query has not been loaded. Please load the query." + ); + } + + const promise = queryRef.fetchMore( + options as FetchMoreQueryOptions + ); + + setPromiseCache((promiseCache) => + new Map(promiseCache).set(queryRef.key, queryRef.promise) + ); + + return promise; + }, + [queryRef] + ); + + const refetch: RefetchFunction = React.useCallback( + (options) => { + if (!queryRef) { + throw new Error( + "The query has not been loaded. Please load the query." + ); + } + + const promise = queryRef.refetch(options); + + setPromiseCache((promiseCache) => + new Map(promiseCache).set(queryRef.key, queryRef.promise) + ); + + return promise; + }, + [queryRef] + ); + + const loadQuery: LoadQueryFunction = React.useCallback( + (...args) => { + invariant( + !calledDuringRender(), + "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." + ); + + const [variables] = args; + + const cacheKey: CacheKey = [ + query, + canonicalStringify(variables), + ...([] as any[]).concat(queryKey), + ]; + + const queryRef = suspenseCache.getQueryRef(cacheKey, () => + client.watchQuery({ + ...watchQueryOptions, + variables, + } as WatchQueryOptions) + ); + + promiseCache.set(queryRef.key, queryRef.promise); + setQueryRef(queryRef); + }, + [ + query, + queryKey, + suspenseCache, + watchQueryOptions, + promiseCache, + calledDuringRender, + ] + ); + + const reset: ResetFunction = React.useCallback(() => { + setQueryRef(null); + }, [queryRef]); + + return React.useMemo(() => { + return [ + loadQuery, + queryRef && wrapQueryRef(queryRef), + { fetchMore, refetch, reset }, + ]; + }, [queryRef, loadQuery, fetchMore, refetch, reset]); +} diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 70df3b03458..f6f7af613aa 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -20,6 +20,8 @@ import type { InternalRefetchQueriesInclude, WatchQueryOptions, WatchQueryFetchPolicy, + ErrorPolicy, + RefetchWritePolicy, } from "../../core/index.js"; /* QueryReference type */ @@ -188,6 +190,67 @@ export interface BackgroundQueryHookOptions< skip?: boolean; } +export type LoadableQueryHookFetchPolicy = Extract< + WatchQueryFetchPolicy, + "cache-first" | "network-only" | "no-cache" | "cache-and-network" +>; + +export interface LoadableQueryHookOptions { + /** + * Whether to canonize cache results before returning them. Canonization + * takes some extra time, but it speeds up future deep equality comparisons. + * Defaults to false. + */ + canonizeResults?: boolean; + /** + * The instance of {@link ApolloClient} to use to execute the query. + * + * By default, the instance that's passed down via context is used, but you + * can provide a different instance here. + */ + client?: ApolloClient; + /** + * Context to be passed to link execution chain + */ + context?: DefaultContext; + /** + * Specifies the {@link ErrorPolicy} to be used for this query + */ + errorPolicy?: ErrorPolicy; + /** + * + * Specifies how the query interacts with the Apollo Client cache during + * execution (for example, whether it checks the cache for results before + * sending a request to the server). + * + * For details, see {@link https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy | Setting a fetch policy}. + * + * The default value is `cache-first`. + */ + fetchPolicy?: LoadableQueryHookFetchPolicy; + /** + * A unique identifier for the query. Each item in the array must be a stable + * identifier to prevent infinite fetches. + * + * This is useful when using the same query and variables combination in more + * than one component, otherwise the components may clobber each other. This + * can also be used to force the query to re-evaluate fresh. + */ + queryKey?: string | number | any[]; + /** + * Specifies whether a {@link NetworkStatus.refetch} operation should merge + * incoming field data with existing data, or overwrite the existing data. + * Overwriting is probably preferable, but merging is currently the default + * behavior, for backwards compatibility with Apollo Client 3.x. + */ + refetchWritePolicy?: RefetchWritePolicy; + /** + * Allow returning incomplete data from the cache when a larger query cannot + * be fully satisfied by the cache, instead of returning nothing. + */ + returnPartialData?: boolean; +} + /** * @deprecated TODO Delete this unused interface. */ diff --git a/src/testing/internal/disposables/disableActWarnings.ts b/src/testing/internal/disposables/disableActWarnings.ts new file mode 100644 index 00000000000..c5254c8dc1d --- /dev/null +++ b/src/testing/internal/disposables/disableActWarnings.ts @@ -0,0 +1,15 @@ +import { withCleanup } from "./withCleanup.js"; + +/** + * Temporarily disable act warnings. + * + * https://github.com/reactwg/react-18/discussions/102 + */ +export function disableActWarnings() { + const prev = { prevActEnv: (globalThis as any).IS_REACT_ACT_ENVIRONMENT }; + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = false; + + return withCleanup(prev, ({ prevActEnv }) => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = prevActEnv; + }); +} diff --git a/src/testing/internal/disposables/index.ts b/src/testing/internal/disposables/index.ts index 6d232565db4..9895d129589 100644 --- a/src/testing/internal/disposables/index.ts +++ b/src/testing/internal/disposables/index.ts @@ -1,2 +1,3 @@ +export { disableActWarnings } from "./disableActWarnings.js"; export { spyOnConsole } from "./spyOnConsole.js"; export { withCleanup } from "./withCleanup.js"; diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index 24b4737c2c0..c077c63fac3 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -62,6 +62,8 @@ export interface Render extends BaseRender { * ``` */ withinDOM: () => SyncScreen; + + renderedComponents: Array; } /** @internal */ @@ -77,7 +79,8 @@ export class RenderInstance implements Render { constructor( baseRender: BaseRender, public snapshot: Snapshot, - private stringifiedDOM: string | undefined + private stringifiedDOM: string | undefined, + public renderedComponents: Array ) { this.id = baseRender.id; this.phase = baseRender.phase; diff --git a/src/testing/internal/profile/context.tsx b/src/testing/internal/profile/context.tsx new file mode 100644 index 00000000000..a8488e73a6c --- /dev/null +++ b/src/testing/internal/profile/context.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; + +export interface ProfilerContextValue { + renderedComponents: Array; +} + +const ProfilerContext = React.createContext( + undefined +); + +export function ProfilerContextProvider({ + children, + value, +}: { + children: React.ReactNode; + value: ProfilerContextValue; +}) { + const parentContext = useProfilerContext(); + + if (parentContext) { + throw new Error("Profilers should not be nested in the same tree"); + } + + return ( + + {children} + + ); +} + +export function useProfilerContext() { + return React.useContext(ProfilerContext); +} diff --git a/src/testing/internal/profile/index.ts b/src/testing/internal/profile/index.ts index 01bb526c52c..3d9ddd55559 100644 --- a/src/testing/internal/profile/index.ts +++ b/src/testing/internal/profile/index.ts @@ -1,8 +1,15 @@ export type { NextRenderOptions, + Profiler, ProfiledComponent, ProfiledHook, } from "./profile.js"; -export { profile, profileHook, WaitForRenderTimeoutError } from "./profile.js"; +export { + createProfiler, + profile, + profileHook, + useTrackRenders, + WaitForRenderTimeoutError, +} from "./profile.js"; export type { SyncScreen } from "./Render.js"; diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 8ae43b64c01..4b2717dc21d 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -8,6 +8,9 @@ global.TextDecoder ??= TextDecoder; import type { Render, BaseRender } from "./Render.js"; import { RenderInstance } from "./Render.js"; import { applyStackTrace, captureStackTrace } from "./traces.js"; +import type { ProfilerContextValue } from "./context.js"; +import { ProfilerContextProvider, useProfilerContext } from "./context.js"; +import { disableActWarnings } from "../disposables/index.js"; type ValidSnapshot = void | (object & { /* not a function */ call?: never }); @@ -20,10 +23,15 @@ export interface NextRenderOptions { } /** @internal */ -export interface ProfiledComponent - extends React.FC, - ProfiledComponentFields, - ProfiledComponentOnlyFields {} +interface ProfilerProps { + children: React.ReactNode; +} + +/** @internal */ +export interface Profiler + extends React.FC, + ProfiledComponentFields, + ProfiledComponentOnlyFields {} interface ReplaceSnapshot { (newSnapshot: Snapshot): void; @@ -39,13 +47,13 @@ interface MergeSnapshot { ): void; } -interface ProfiledComponentOnlyFields { +interface ProfiledComponentOnlyFields { // Allows for partial updating of the snapshot by shallow merging the results mergeSnapshot: MergeSnapshot; // Performs a full replacement of the snapshot replaceSnapshot: ReplaceSnapshot; } -interface ProfiledComponentFields { +interface ProfiledComponentFields { /** * An array of all renders that have happened so far. * Errors thrown during component render will be captured here, too. @@ -81,17 +89,58 @@ interface ProfiledComponentFields { waitForNextRender(options?: NextRenderOptions): Promise>; } +export interface ProfiledComponent + extends React.FC, + ProfiledComponentFields, + ProfiledComponentOnlyFields {} + /** @internal */ -export function profile< - Snapshot extends ValidSnapshot = void, - Props = Record, ->({ +export function profile({ Component, + ...options +}: { + onRender?: ( + info: BaseRender & { + snapshot: Snapshot; + replaceSnapshot: ReplaceSnapshot; + mergeSnapshot: MergeSnapshot; + } + ) => void; + Component: React.ComponentType; + snapshotDOM?: boolean; + initialSnapshot?: Snapshot; +}): ProfiledComponent { + const Profiler = createProfiler(options); + + return Object.assign( + function ProfiledComponent(props: Props) { + return ( + + + + ); + }, + { + mergeSnapshot: Profiler.mergeSnapshot, + replaceSnapshot: Profiler.replaceSnapshot, + getCurrentRender: Profiler.getCurrentRender, + peekRender: Profiler.peekRender, + takeRender: Profiler.takeRender, + totalRenderCount: Profiler.totalRenderCount, + waitForNextRender: Profiler.waitForNextRender, + get renders() { + return Profiler.renders; + }, + } + ); +} + +/** @internal */ +export function createProfiler({ onRender, snapshotDOM = false, initialSnapshot, }: { - Component: React.ComponentType; onRender?: ( info: BaseRender & { snapshot: Snapshot; @@ -101,7 +150,7 @@ export function profile< ) => void; snapshotDOM?: boolean; initialSnapshot?: Snapshot; -}) { +} = {}) { let nextRender: Promise> | undefined; let resolveNextRender: ((render: Render) => void) | undefined; let rejectNextRender: ((error: unknown) => void) | undefined; @@ -133,6 +182,10 @@ export function profile< })); }; + const profilerContext: ProfilerContextValue = { + renderedComponents: [], + }; + const profilerOnRender: React.ProfilerOnRenderCallback = ( id, phase, @@ -148,7 +201,7 @@ export function profile< baseDuration, startTime, commitTime, - count: Profiled.renders.length + 1, + count: Profiler.renders.length + 1, }; try { /* @@ -169,13 +222,19 @@ export function profile< const domSnapshot = snapshotDOM ? window.document.body.innerHTML : undefined; - const render = new RenderInstance(baseRender, snapshot, domSnapshot); - Profiled.renders.push(render); + const render = new RenderInstance( + baseRender, + snapshot, + domSnapshot, + profilerContext.renderedComponents + ); + profilerContext.renderedComponents = []; + Profiler.renders.push(render); resolveNextRender?.(render); } catch (error) { - Profiled.renders.push({ + Profiler.renders.push({ phase: "snapshotError", - count: Profiled.renders.length, + count: Profiler.renders.length, error, }); rejectNextRender?.(error); @@ -185,27 +244,31 @@ export function profile< }; let iteratorPosition = 0; - const Profiled: ProfiledComponent = Object.assign( - (props: Props) => ( - - - - ), + const Profiler: Profiler = Object.assign( + ({ children }: ProfilerProps) => { + return ( + + + {children} + + + ); + }, { replaceSnapshot, mergeSnapshot, - } satisfies ProfiledComponentOnlyFields, + } satisfies ProfiledComponentOnlyFields, { renders: new Array< | Render | { phase: "snapshotError"; count: number; error: unknown } >(), totalRenderCount() { - return Profiled.renders.length; + return Profiler.renders.length; }, async peekRender(options: NextRenderOptions = {}) { - if (iteratorPosition < Profiled.renders.length) { - const render = Profiled.renders[iteratorPosition]; + if (iteratorPosition < Profiler.renders.length) { + const render = Profiler.renders[iteratorPosition]; if (render.phase === "snapshotError") { throw render.error; @@ -213,16 +276,22 @@ export function profile< return render; } - return Profiled.waitForNextRender({ - [_stackTrace]: captureStackTrace(Profiled.peekRender), + return Profiler.waitForNextRender({ + [_stackTrace]: captureStackTrace(Profiler.peekRender), ...options, }); }, async takeRender(options: NextRenderOptions = {}) { + // In many cases we do not control the resolution of the suspended + // promise which results in noisy tests when the profiler due to + // repeated act warnings. + using _disabledActWarnings = disableActWarnings(); + let error: unknown = undefined; + try { - return await Profiled.peekRender({ - [_stackTrace]: captureStackTrace(Profiled.takeRender), + return await Profiler.peekRender({ + [_stackTrace]: captureStackTrace(Profiler.takeRender), ...options, }); } catch (e) { @@ -248,7 +317,7 @@ export function profile< ); } - const render = Profiled.renders[currentPosition]; + const render = Profiler.renders[currentPosition]; if (render.phase === "snapshotError") { throw render.error; @@ -259,7 +328,7 @@ export function profile< timeout = 1000, // capture the stack trace here so its stack trace is as close to the calling code as possible [_stackTrace]: stackTrace = captureStackTrace( - Profiled.waitForNextRender + Profiler.waitForNextRender ), }: NextRenderOptions = {}) { if (!nextRender) { @@ -281,9 +350,9 @@ export function profile< } return nextRender; }, - } satisfies ProfiledComponentFields + } satisfies ProfiledComponentFields ); - return Profiled; + return Profiler; } /** @internal */ @@ -305,59 +374,86 @@ type ResultReplaceRenderWithSnapshot = T extends ( ? (...args: Args) => Promise : T; -type ProfiledHookFields = ProfiledComponentFields< - Props, - ReturnValue -> extends infer PC - ? { - [K in keyof PC as StringReplaceRenderWithSnapshot< - K & string - >]: ResultReplaceRenderWithSnapshot; - } - : never; +type ProfiledHookFields = + ProfiledComponentFields extends infer PC + ? { + [K in keyof PC as StringReplaceRenderWithSnapshot< + K & string + >]: ResultReplaceRenderWithSnapshot; + } + : never; /** @internal */ export interface ProfiledHook extends React.FC, - ProfiledHookFields { - ProfiledComponent: ProfiledComponent; + ProfiledHookFields { + Profiler: Profiler; } /** @internal */ export function profileHook( renderCallback: (props: Props) => ReturnValue ): ProfiledHook { - let returnValue: ReturnValue; - const Component = (props: Props) => { - ProfiledComponent.replaceSnapshot(renderCallback(props)); + const Profiler = createProfiler(); + + const ProfiledHook = (props: Props) => { + Profiler.replaceSnapshot(renderCallback(props)); return null; }; - const ProfiledComponent = profile({ - Component, - onRender: () => returnValue, - }); + return Object.assign( - function ProfiledHook(props: Props) { - return ; + function App(props: Props) { + return ( + + + + ); }, { - ProfiledComponent, + Profiler, }, { - renders: ProfiledComponent.renders, - totalSnapshotCount: ProfiledComponent.totalRenderCount, + renders: Profiler.renders, + totalSnapshotCount: Profiler.totalRenderCount, async peekSnapshot(options) { - return (await ProfiledComponent.peekRender(options)).snapshot; + return (await Profiler.peekRender(options)).snapshot; }, async takeSnapshot(options) { - return (await ProfiledComponent.takeRender(options)).snapshot; + return (await Profiler.takeRender(options)).snapshot; }, getCurrentSnapshot() { - return ProfiledComponent.getCurrentRender().snapshot; + return Profiler.getCurrentRender().snapshot; }, async waitForNextSnapshot(options) { - return (await ProfiledComponent.waitForNextRender(options)).snapshot; + return (await Profiler.waitForNextRender(options)).snapshot; }, - } satisfies ProfiledHookFields + } satisfies ProfiledHookFields ); } + +function resolveHookOwner(): React.ComponentType | undefined { + return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED + ?.ReactCurrentOwner?.current?.elementType; +} + +export function useTrackRenders({ name }: { name?: string } = {}) { + const component = name || resolveHookOwner(); + + if (!component) { + throw new Error( + "useTrackRender: Unable to determine component. Please ensure the hook is called inside a rendered component or provide a `name` option." + ); + } + + const ctx = useProfilerContext(); + + if (!ctx) { + throw new Error( + "useTrackComponentRender: A Profiler must be created and rendered to track component renders" + ); + } + + React.useLayoutEffect(() => { + ctx.renderedComponents.unshift(component); + }); +} diff --git a/src/testing/matchers/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index 8a4e72025a9..435c24a29a6 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -2,22 +2,22 @@ import type { MatcherFunction } from "expect"; import { WaitForRenderTimeoutError } from "../internal/index.js"; import type { NextRenderOptions, + Profiler, ProfiledComponent, ProfiledHook, } from "../internal/index.js"; + export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = async function (actual, options) { - const _profiled = actual as + const _profiler = actual as + | Profiler | ProfiledComponent | ProfiledHook; - const profiled = - "ProfiledComponent" in _profiled - ? _profiled.ProfiledComponent - : _profiled; - const hint = this.utils.matcherHint("toRerender"); + const profiler = "Profiler" in _profiler ? _profiler.Profiler : _profiler; + const hint = this.utils.matcherHint("toRerender", "ProfiledComponent", ""); let pass = true; try { - await profiled.peekRender({ timeout: 100, ...options }); + await profiler.peekRender({ timeout: 100, ...options }); } catch (e) { if (e instanceof WaitForRenderTimeoutError) { pass = false; @@ -25,12 +25,13 @@ export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = throw e; } } + return { pass, message() { return ( hint + - ` Expected component to${pass ? " not" : ""} rerender, ` + + `\n\nExpected component to${pass ? " not" : ""} rerender, ` + `but it did${pass ? "" : " not"}.` ); }, @@ -43,28 +44,28 @@ const failed = {}; export const toRenderExactlyTimes: MatcherFunction< [times: number, options?: NextRenderOptions] > = async function (actual, times, optionsPerRender) { - const _profiled = actual as + const _profiler = actual as + | Profiler | ProfiledComponent | ProfiledHook; - const profiled = - "ProfiledComponent" in _profiled ? _profiled.ProfiledComponent : _profiled; + const profiler = "Profiler" in _profiler ? _profiler.Profiler : _profiler; const options = { timeout: 100, ...optionsPerRender }; const hint = this.utils.matcherHint("toRenderExactlyTimes"); let pass = true; try { - if (profiled.totalRenderCount() > times) { + if (profiler.totalRenderCount() > times) { throw failed; } try { - while (profiled.totalRenderCount() < times) { - await profiled.waitForNextRender(options); + while (profiler.totalRenderCount() < times) { + await profiler.waitForNextRender(options); } } catch (e) { // timeouts here should just fail the test, rethrow other errors throw e instanceof WaitForRenderTimeoutError ? failed : e; } try { - await profiled.waitForNextRender(options); + await profiler.waitForNextRender(options); } catch (e) { // we are expecting a timeout here, so swallow that error, rethrow others if (!(e instanceof WaitForRenderTimeoutError)) { @@ -84,7 +85,7 @@ export const toRenderExactlyTimes: MatcherFunction< return ( hint + ` Expected component to${pass ? " not" : ""} render exactly ${times}.` + - ` It rendered ${profiled.totalRenderCount()} times.` + ` It rendered ${profiler.totalRenderCount()} times.` ); }, }; diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 715f7d3dbdf..b09a823dc25 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -5,6 +5,7 @@ import type { } from "../../core/index.js"; import { NextRenderOptions, + Profiler, ProfiledComponent, ProfiledHook, } from "../internal/index.js"; @@ -29,11 +30,15 @@ interface ApolloCustomMatchers { ) => R : { error: "matcher needs to be called on an ApolloClient instance" }; - toRerender: T extends ProfiledComponent | ProfiledHook + toRerender: T extends + | Profiler + | ProfiledComponent + | ProfiledHook ? (options?: NextRenderOptions) => Promise : { error: "matcher needs to be called on a ProfiledComponent instance" }; toRenderExactlyTimes: T extends + | Profiler | ProfiledComponent | ProfiledHook ? (count: number, options?: NextRenderOptions) => Promise diff --git a/src/utilities/index.ts b/src/utilities/index.ts index da8affb4b5a..ec05d2aa043 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -129,3 +129,4 @@ export { stripTypename } from "./common/stripTypename.js"; export * from "./types/IsStrictlyAny.js"; export type { DeepOmit } from "./types/DeepOmit.js"; export type { DeepPartial } from "./types/DeepPartial.js"; +export type { OnlyRequiredProperties } from "./types/OnlyRequiredProperties.js"; diff --git a/src/utilities/types/OnlyRequiredProperties.ts b/src/utilities/types/OnlyRequiredProperties.ts new file mode 100644 index 00000000000..5264a0fca69 --- /dev/null +++ b/src/utilities/types/OnlyRequiredProperties.ts @@ -0,0 +1,6 @@ +/** + * Returns a new type that only contains the required properties from `T` + */ +export type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; From cc4ac7e1917f046bcd177882727864eed40b910e Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 Nov 2023 04:50:50 +0100 Subject: [PATCH 33/90] Address potential memory leaks in `FragmentRegistry` (#11356) --- .changeset/clean-items-smash.md | 5 +++++ src/cache/inmemory/fragmentRegistry.ts | 14 +++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 .changeset/clean-items-smash.md diff --git a/.changeset/clean-items-smash.md b/.changeset/clean-items-smash.md new file mode 100644 index 00000000000..c0111542c78 --- /dev/null +++ b/.changeset/clean-items-smash.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix a potential memory leak in `FragmentRegistry.transform` and `FragmentRegistry.findFragmentSpreads` that would hold on to passed-in `DocumentNodes` for too long. diff --git a/src/cache/inmemory/fragmentRegistry.ts b/src/cache/inmemory/fragmentRegistry.ts index f7db169e3b0..12cade01aea 100644 --- a/src/cache/inmemory/fragmentRegistry.ts +++ b/src/cache/inmemory/fragmentRegistry.ts @@ -6,7 +6,6 @@ import type { } from "graphql"; import { visit } from "graphql"; -import type { OptimisticWrapperFunction } from "optimism"; import { wrap } from "optimism"; import type { FragmentMap } from "../../utilities/index.js"; @@ -66,15 +65,12 @@ class FragmentRegistry implements FragmentRegistryAPI { private invalidate(name: string) {} public resetCaches() { - this.invalidate = (this.lookup = this.cacheUnaryMethod(this.lookup)).dirty; // This dirty function is bound to the wrapped lookup method. - this.transform = this.cacheUnaryMethod(this.transform); - this.findFragmentSpreads = this.cacheUnaryMethod(this.findFragmentSpreads); - } - - private cacheUnaryMethod any>(originalMethod: F) { - return wrap, ReturnType>(originalMethod.bind(this), { + const proto = FragmentRegistry.prototype; + this.invalidate = (this.lookup = wrap(proto.lookup.bind(this), { makeCacheKey: (arg) => arg, - }) as OptimisticWrapperFunction, ReturnType> & F; + })).dirty; // This dirty function is bound to the wrapped lookup method. + this.transform = wrap(proto.transform.bind(this)); + this.findFragmentSpreads = wrap(proto.findFragmentSpreads.bind(this)); } public lookup(fragmentName: string): FragmentDefinitionNode | null { From 30d17bfebe44dbfa7b78c8982cfeb49afd37129c Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 Nov 2023 11:46:47 +0100 Subject: [PATCH 34/90] `print`: use `WeakCache` instead of `WeakMap` (#11367) Co-authored-by: Jerel Miller Co-authored-by: phryneas --- .changeset/polite-avocados-warn.md | 5 +++++ .size-limit.cjs | 1 + .size-limits.json | 4 ++-- package-lock.json | 12 ++++++++++++ package.json | 1 + src/utilities/graphql/print.ts | 15 ++++++++------- 6 files changed, 29 insertions(+), 9 deletions(-) create mode 100644 .changeset/polite-avocados-warn.md diff --git a/.changeset/polite-avocados-warn.md b/.changeset/polite-avocados-warn.md new file mode 100644 index 00000000000..dd04015cf3d --- /dev/null +++ b/.changeset/polite-avocados-warn.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`print`: use `WeakCache` instead of `WeakMap` diff --git a/.size-limit.cjs b/.size-limit.cjs index 7c7b71da42f..6faa1c00aca 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -36,6 +36,7 @@ const checks = [ "react", "react-dom", "@graphql-typed-document-node/core", + "@wry/caches", "@wry/context", "@wry/equality", "@wry/trie", diff --git a/.size-limits.json b/.size-limits.json index 7bc50667da7..d5dd8296590 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38600, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32187 + "dist/apollo-client.min.cjs": 38603, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32203 } diff --git a/package-lock.json b/package-lock.json index 886832fd3e9..e879e9a11bd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", "@wry/context": "^0.7.3", "@wry/equality": "^0.5.6", "@wry/trie": "^0.5.0", @@ -3294,6 +3295,17 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@wry/caches": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.0.tgz", + "integrity": "sha512-FHRUDe2tqrXAj6A/1D39No68lFWbbnh+NCpG9J/6idhL/2Mb/AaxBTYg/sbUVImEo8a4mWeOewUlB1W7uLjByA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@wry/context": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.3.tgz", diff --git a/package.json b/package.json index 29972ae6010..64adc0f0f3c 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ }, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", "@wry/context": "^0.7.3", "@wry/equality": "^0.5.6", "@wry/trie": "^0.5.0", diff --git a/src/utilities/graphql/print.ts b/src/utilities/graphql/print.ts index d90a15611d0..3ba1134c968 100644 --- a/src/utilities/graphql/print.ts +++ b/src/utilities/graphql/print.ts @@ -1,23 +1,24 @@ import type { ASTNode } from "graphql"; import { print as origPrint } from "graphql"; -import { canUseWeakMap } from "../common/canUse.js"; +import { WeakCache } from "@wry/caches"; -let printCache: undefined | WeakMap; -// further TODO: replace with `optimism` with a `WeakCache` once those are available +let printCache!: WeakCache; export const print = Object.assign( (ast: ASTNode) => { - let result; - result = printCache?.get(ast); + let result = printCache.get(ast); if (!result) { result = origPrint(ast); - printCache?.set(ast, result); + printCache.set(ast, result); } return result; }, { reset() { - printCache = canUseWeakMap ? new WeakMap() : undefined; + printCache = new WeakCache< + ASTNode, + string + >(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); }, } ); From 25e2cb431c76ec5aa88202eaacbd98fad42edc7f Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 Nov 2023 12:06:15 +0100 Subject: [PATCH 35/90] `parse` function: improve memory management (#11370) Co-authored-by: phryneas --- .api-reports/api-report-react.md | 6 ++++++ .api-reports/api-report-react_parser.md | 6 ++++++ .api-reports/api-report.md | 6 ++++++ .changeset/cold-llamas-turn.md | 8 ++++++++ .size-limits.json | 2 +- src/react/parser/index.ts | 20 +++++++++++++++++++- 6 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 .changeset/cold-llamas-turn.md diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 9730ae3bc79..65a4bbe7d1f 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -1432,6 +1432,12 @@ type OperationVariables = Record; // @public (undocumented) export function parser(document: DocumentNode): IDocumentDefinition; +// @public (undocumented) +export namespace parser { + var // (undocumented) + resetCache: () => void; +} + // @public (undocumented) type Path = ReadonlyArray; diff --git a/.api-reports/api-report-react_parser.md b/.api-reports/api-report-react_parser.md index 96d961506af..9dd7eab4a8b 100644 --- a/.api-reports/api-report-react_parser.md +++ b/.api-reports/api-report-react_parser.md @@ -34,6 +34,12 @@ export function operationName(type: DocumentType_2): string; // @public (undocumented) export function parser(document: DocumentNode): IDocumentDefinition; +// @public (undocumented) +export namespace parser { + var // (undocumented) + resetCache: () => void; +} + // @public (undocumented) export function verifyDocumentType(document: DocumentNode, type: DocumentType_2): void; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index ae6e27d72e9..9e336a0c50a 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -1931,6 +1931,12 @@ export function parseAndCheckHttpResponse(operations: Operation | Operation[]): // @public (undocumented) export function parser(document: DocumentNode): IDocumentDefinition; +// @public (undocumented) +export namespace parser { + var // (undocumented) + resetCache: () => void; +} + // @public (undocumented) export type Path = ReadonlyArray; diff --git a/.changeset/cold-llamas-turn.md b/.changeset/cold-llamas-turn.md new file mode 100644 index 00000000000..a3f1e0099df --- /dev/null +++ b/.changeset/cold-llamas-turn.md @@ -0,0 +1,8 @@ +--- +"@apollo/client": patch +--- + +`parse` function: improve memory management +* use LRU `WeakCache` instead of `Map` to keep a limited number of parsed results +* cache is initiated lazily, only when needed +* expose `parse.resetCache()` method diff --git a/.size-limits.json b/.size-limits.json index d5dd8296590..ee06be421b2 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38603, + "dist/apollo-client.min.cjs": 38625, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32203 } diff --git a/src/react/parser/index.ts b/src/react/parser/index.ts index 984b491142f..cbaa8f69612 100644 --- a/src/react/parser/index.ts +++ b/src/react/parser/index.ts @@ -1,3 +1,4 @@ +import { WeakCache } from "@wry/caches"; import { invariant } from "../../utilities/globals/index.js"; import type { @@ -19,7 +20,16 @@ export interface IDocumentDefinition { variables: ReadonlyArray; } -const cache = new Map(); +let cache: + | undefined + | WeakCache< + DocumentNode, + { + name: string; + type: DocumentType; + variables: readonly VariableDefinitionNode[]; + } + >; export function operationName(type: DocumentType) { let name; @@ -39,6 +49,10 @@ export function operationName(type: DocumentType) { // This parser is mostly used to safety check incoming documents. export function parser(document: DocumentNode): IDocumentDefinition { + if (!cache) { + cache = + new WeakCache(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); + } const cached = cache.get(document); if (cached) return cached; @@ -131,6 +145,10 @@ export function parser(document: DocumentNode): IDocumentDefinition { return payload; } +parser.resetCache = () => { + cache = undefined; +}; + export function verifyDocumentType(document: DocumentNode, type: DocumentType) { const operation = parser(document); const requiredOperationName = operationName(type); From 4dce8673b1757d8a3a4edd2996d780e86fad14e3 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 Nov 2023 12:19:14 +0100 Subject: [PATCH 36/90] `QueryManager.transformCache`: use `WeakCache` (#11387) Co-authored-by: phryneas --- .changeset/shaggy-sheep-pull.md | 5 +++++ .size-limits.json | 4 ++-- src/core/QueryManager.ts | 6 +++--- 3 files changed, 10 insertions(+), 5 deletions(-) create mode 100644 .changeset/shaggy-sheep-pull.md diff --git a/.changeset/shaggy-sheep-pull.md b/.changeset/shaggy-sheep-pull.md new file mode 100644 index 00000000000..9c4ac23123b --- /dev/null +++ b/.changeset/shaggy-sheep-pull.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`QueryManager.transformCache`: use `WeakCache` instead of `WeakMap` diff --git a/.size-limits.json b/.size-limits.json index ee06be421b2..203fbb531d2 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38625, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32203 + "dist/apollo-client.min.cjs": 38630, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32213 } diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 98ab4d1d1f4..f6c471ef840 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -4,6 +4,7 @@ import type { DocumentNode } from "graphql"; // TODO(brian): A hack until this issue is resolved (https://github.com/graphql/graphql-js/issues/3356) type OperationTypeNode = any; import { equal } from "@wry/equality"; +import { WeakCache } from "@wry/caches"; import type { ApolloLink, FetchResult } from "../link/core/index.js"; import { execute } from "../link/core/index.js"; @@ -27,7 +28,6 @@ import { hasClientExports, graphQLResultHasError, getGraphQLErrorsFromResult, - canUseWeakMap, Observable, asyncMap, isNonEmptyArray, @@ -651,10 +651,10 @@ export class QueryManager { return this.documentTransform.transformDocument(document); } - private transformCache = new (canUseWeakMap ? WeakMap : Map)< + private transformCache = new WeakCache< DocumentNode, TransformCacheEntry - >(); + >(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); public getDocumentInfo(document: DocumentNode) { const { transformCache } = this; From 7d939f80fbc2c419c58a6c55b6a35ee7474d0379 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 30 Nov 2023 15:51:57 +0100 Subject: [PATCH 37/90] fix potential memory leak in `Concast`, add tests (#11358) --- .changeset/forty-cups-shop.md | 5 + .size-limits.json | 4 +- package.json | 2 +- src/testing/matchers/index.d.ts | 4 + src/testing/matchers/index.ts | 2 + src/testing/matchers/toBeGarbageCollected.ts | 59 +++++++++ src/tsconfig.json | 2 +- src/utilities/observables/Concast.ts | 5 +- .../observables/__tests__/Concast.ts | 113 +++++++++++++++++- 9 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 .changeset/forty-cups-shop.md create mode 100644 src/testing/matchers/toBeGarbageCollected.ts diff --git a/.changeset/forty-cups-shop.md b/.changeset/forty-cups-shop.md new file mode 100644 index 00000000000..2c576843fdd --- /dev/null +++ b/.changeset/forty-cups-shop.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fixes a potential memory leak in `Concast` that might have been triggered when `Concast` was used outside of Apollo Client. diff --git a/.size-limits.json b/.size-limits.json index 203fbb531d2..e196670f752 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38630, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32213 + "dist/apollo-client.min.cjs": 38632, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32215 } diff --git a/package.json b/package.json index 64adc0f0f3c..f857236fb70 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "ci:precheck": "node config/precheck.js", "format": "prettier --write .", "lint": "eslint 'src/**/*.{[jt]s,[jt]sx}'", - "test": "jest --config ./config/jest.config.js", + "test": "node --expose-gc ./node_modules/jest/bin/jest.js --config ./config/jest.config.js", "test:debug": "node --inspect-brk node_modules/.bin/jest --config ./config/jest.config.js --runInBand --testTimeout 99999 --logHeapUsage", "test:ci": "TEST_ENV=ci npm run test:coverage -- --logHeapUsage && npm run test:memory", "test:watch": "jest --config ./config/jest.config.js --watch", diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index b09a823dc25..65b10ba4fc8 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -43,6 +43,10 @@ interface ApolloCustomMatchers { | ProfiledHook ? (count: number, options?: NextRenderOptions) => Promise : { error: "matcher needs to be called on a ProfiledComponent instance" }; + + toBeGarbageCollected: T extends WeakRef + ? () => Promise + : { error: "matcher needs to be called on a WeakRef instance" }; } declare global { diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index d2ebd8ce7c2..709bfbad53b 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -2,10 +2,12 @@ import { expect } from "@jest/globals"; import { toMatchDocument } from "./toMatchDocument.js"; import { toHaveSuspenseCacheEntryUsing } from "./toHaveSuspenseCacheEntryUsing.js"; import { toRerender, toRenderExactlyTimes } from "./ProfiledComponent.js"; +import { toBeGarbageCollected } from "./toBeGarbageCollected.js"; expect.extend({ toHaveSuspenseCacheEntryUsing, toMatchDocument, toRerender, toRenderExactlyTimes, + toBeGarbageCollected, }); diff --git a/src/testing/matchers/toBeGarbageCollected.ts b/src/testing/matchers/toBeGarbageCollected.ts new file mode 100644 index 00000000000..fda58543b38 --- /dev/null +++ b/src/testing/matchers/toBeGarbageCollected.ts @@ -0,0 +1,59 @@ +import type { MatcherFunction } from "expect"; + +// this is necessary because this file is picked up by `tsc` (it's not a test), +// but our main `tsconfig.json` doesn't include `"ES2021.WeakRef"` on purpose +declare class WeakRef { + constructor(target: T); + deref(): T | undefined; +} + +export const toBeGarbageCollected: MatcherFunction<[weakRef: WeakRef]> = + async function (actual) { + const hint = this.utils.matcherHint("toBeGarbageCollected"); + + if (!(actual instanceof WeakRef)) { + throw new Error( + hint + + "\n\n" + + `Expected value to be a WeakRef, but it was a ${typeof actual}.` + ); + } + + let pass = false; + let interval: NodeJS.Timeout | undefined; + let timeout: NodeJS.Timeout | undefined; + await Promise.race([ + new Promise((resolve) => { + timeout = setTimeout(resolve, 1000); + }), + new Promise((resolve) => { + interval = setInterval(() => { + global.gc!(); + pass = actual.deref() === undefined; + if (pass) { + resolve(); + } + }, 1); + }), + ]); + + clearInterval(interval); + clearTimeout(timeout); + + return { + pass, + message: () => { + if (pass) { + return ( + hint + + "\n\n" + + "Expected value to not be cache-collected, but it was." + ); + } + + return ( + hint + "\n\n Expected value to be cache-collected, but it was not." + ); + }, + }; + }; diff --git a/src/tsconfig.json b/src/tsconfig.json index 321f038a735..40ade0f5761 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -5,7 +5,7 @@ { "compilerOptions": { "noEmit": true, - "lib": ["es2015", "esnext.asynciterable", "dom"], + "lib": ["es2015", "esnext.asynciterable", "dom", "ES2021.WeakRef"], "types": ["jest", "node", "./testing/matchers/index.d.ts"] }, "extends": "../tsconfig.json", diff --git a/src/utilities/observables/Concast.ts b/src/utilities/observables/Concast.ts index c2fbb6fb180..73c36520f8b 100644 --- a/src/utilities/observables/Concast.ts +++ b/src/utilities/observables/Concast.ts @@ -210,7 +210,10 @@ export class Concast extends Observable { // followed by a 'complete' message (see addObserver). iterateObserversSafely(this.observers, "complete"); } else if (isPromiseLike(value)) { - value.then((obs) => (this.sub = obs.subscribe(this.handlers))); + value.then( + (obs) => (this.sub = obs.subscribe(this.handlers)), + this.handlers.error + ); } else { this.sub = value.subscribe(this.handlers); } diff --git a/src/utilities/observables/__tests__/Concast.ts b/src/utilities/observables/__tests__/Concast.ts index d6ce248159f..b590cde2fb8 100644 --- a/src/utilities/observables/__tests__/Concast.ts +++ b/src/utilities/observables/__tests__/Concast.ts @@ -1,5 +1,5 @@ import { itAsync } from "../../../testing/core"; -import { Observable } from "../Observable"; +import { Observable, Observer } from "../Observable"; import { Concast, ConcastSourcesIterable } from "../Concast"; describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { @@ -187,4 +187,115 @@ describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { sub.unsubscribe(); }); }); + + it("resolving all sources of a concast frees all observer references on `this.observers`", async () => { + const { promise, resolve } = deferred>(); + const observers: Observer[] = [{ next() {} }]; + const observerRefs = observers.map((observer) => new WeakRef(observer)); + + const concast = new Concast([Observable.of(1, 2), promise]); + + concast.subscribe(observers[0]); + delete observers[0]; + + expect(concast["observers"].size).toBe(1); + + resolve(Observable.of(3, 4)); + + await expect(concast.promise).resolves.toBe(4); + + await expect(observerRefs[0]).toBeGarbageCollected(); + }); + + it("rejecting a source-wrapping promise of a concast frees all observer references on `this.observers`", async () => { + const { promise, reject } = deferred>(); + let subscribingObserver: Observer | undefined = { + next() {}, + error() {}, + }; + const subscribingObserverRef = new WeakRef(subscribingObserver); + + const concast = new Concast([ + Observable.of(1, 2), + promise, + // just to ensure this also works if the cancelling source is not the last source + Observable.of(3, 5), + ]); + + concast.subscribe(subscribingObserver); + + expect(concast["observers"].size).toBe(1); + + reject("error"); + await expect(concast.promise).rejects.toBe("error"); + subscribingObserver = undefined; + await expect(subscribingObserverRef).toBeGarbageCollected(); + }); + + it("rejecting a source of a concast frees all observer references on `this.observers`", async () => { + let subscribingObserver: Observer | undefined = { + next() {}, + error() {}, + }; + const subscribingObserverRef = new WeakRef(subscribingObserver); + + let sourceObserver!: Observer; + const sourceObservable = new Observable((o) => { + sourceObserver = o; + }); + + const concast = new Concast([ + Observable.of(1, 2), + sourceObservable, + Observable.of(3, 5), + ]); + + concast.subscribe(subscribingObserver); + + expect(concast["observers"].size).toBe(1); + + await Promise.resolve(); + sourceObserver.error!("error"); + await expect(concast.promise).rejects.toBe("error"); + subscribingObserver = undefined; + await expect(subscribingObserverRef).toBeGarbageCollected(); + }); + + it("after subscribing to an already-resolved concast, the reference is freed up again", async () => { + const concast = new Concast([Observable.of(1, 2)]); + await expect(concast.promise).resolves.toBe(2); + await Promise.resolve(); + + let sourceObserver: Observer | undefined = { next() {}, error() {} }; + const sourceObserverRef = new WeakRef(sourceObserver); + + concast.subscribe(sourceObserver); + + sourceObserver = undefined; + await expect(sourceObserverRef).toBeGarbageCollected(); + }); + + it("after subscribing to an already-rejected concast, the reference is freed up again", async () => { + const concast = new Concast([Promise.reject("error")]); + await expect(concast.promise).rejects.toBe("error"); + await Promise.resolve(); + + let sourceObserver: Observer | undefined = { next() {}, error() {} }; + const sourceObserverRef = new WeakRef(sourceObserver); + + concast.subscribe(sourceObserver); + + sourceObserver = undefined; + await expect(sourceObserverRef).toBeGarbageCollected(); + }); }); + +function deferred() { + let resolve!: (v: X) => void; + let reject!: (e: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { resolve, reject, promise }; +} From 1759066a8f9a204e49228568aef9446a64890ff3 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 1 Dec 2023 11:00:56 +0100 Subject: [PATCH 38/90] `QueryManager.inFlightLinkObservables`: use a strong `Trie` (#11345) Co-authored-by: phryneas --- .api-reports/api-report-core.md | 10 +++--- .api-reports/api-report-react.md | 11 +++--- .api-reports/api-report-react_components.md | 11 +++--- .api-reports/api-report-react_context.md | 11 +++--- .api-reports/api-report-react_hoc.md | 11 +++--- .api-reports/api-report-react_hooks.md | 11 +++--- .api-reports/api-report-react_ssr.md | 11 +++--- .api-reports/api-report-testing.md | 11 +++--- .api-reports/api-report-testing_core.md | 11 +++--- .api-reports/api-report-utilities.md | 10 +++--- .api-reports/api-report.md | 10 +++--- .changeset/sixty-boxes-rest.md | 8 +++++ .size-limits.json | 4 +-- src/core/QueryManager.ts | 37 ++++++++++++--------- src/core/__tests__/QueryManager/index.ts | 11 ++++-- 15 files changed, 115 insertions(+), 63 deletions(-) create mode 100644 .changeset/sixty-boxes-rest.md diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 4a8dfc429e8..3e691e065d6 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -1739,7 +1739,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -2201,9 +2203,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:126:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:394:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:191:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 65a4bbe7d1f..0b90201df27 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -16,6 +16,7 @@ import type { Observer } from 'zen-observable-ts'; import type * as ReactTypes from 'react'; import type { Subscriber } from 'zen-observable-ts'; import type { Subscription } from 'zen-observable-ts'; +import { Trie } from '@wry/trie'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; import type { VariableDefinitionNode } from 'graphql'; @@ -1601,7 +1602,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -2286,9 +2289,9 @@ interface WatchQueryOptions { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1745,9 +1748,9 @@ interface WatchQueryOptions { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1641,9 +1644,9 @@ interface WatchQueryOptions { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1686,9 +1689,9 @@ export function withSubscription { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -2178,9 +2181,9 @@ interface WatchQueryOptions { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1627,9 +1630,9 @@ interface WatchQueryOptions { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1690,9 +1693,9 @@ export function withWarningSpy(it: (...args: TArgs // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:394:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:158:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:160:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 313f20a2f54..0cadba43966 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -15,6 +15,7 @@ import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type { Subscriber } from 'zen-observable-ts'; import type { Subscription } from 'zen-observable-ts'; +import { Trie } from '@wry/trie'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; // Warning: (ae-forgotten-export) The symbol "Modifier" needs to be exported by the entry point index.d.ts @@ -1294,7 +1295,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1646,9 +1649,9 @@ export function withWarningSpy(it: (...args: TArgs // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:394:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:158:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:160:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 3ca8456512a..59c8e4d8380 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -2018,7 +2018,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -2539,9 +2541,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:394:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:158:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:160:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:178:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 9e336a0c50a..04a979da240 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -2156,7 +2156,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -2958,9 +2960,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:126:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:119:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:153:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:384:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:394:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:191:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:26:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts diff --git a/.changeset/sixty-boxes-rest.md b/.changeset/sixty-boxes-rest.md new file mode 100644 index 00000000000..cce6eb7a98a --- /dev/null +++ b/.changeset/sixty-boxes-rest.md @@ -0,0 +1,8 @@ +--- +"@apollo/client": minor +--- + +`QueryManager.inFlightLinkObservables` now uses a strong `Trie` as an internal data structure. + +#### Warning: requires `@apollo/experimental-nextjs-app-support` update +If you are using `@apollo/experimental-nextjs-app-support`, you will need to update that to at least 0.5.2, as it accesses this internal data structure. diff --git a/.size-limits.json b/.size-limits.json index e196670f752..dfe8306d94f 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38632, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32215 + "dist/apollo-client.min.cjs": 38693, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32306 } diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index f6c471ef840..3a94078f3da 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -99,6 +99,7 @@ interface TransformCacheEntry { } import type { DefaultOptions } from "./ApolloClient.js"; +import { Trie } from "@wry/trie"; export class QueryManager { public cache: ApolloCache; @@ -182,6 +183,15 @@ export class QueryManager { if ((this.onBroadcast = onBroadcast)) { this.mutationStore = Object.create(null); } + + // TODO: remove before we release 3.9 + Object.defineProperty(this.inFlightLinkObservables, "get", { + value: () => { + throw new Error( + "This version of Apollo Client requires at least @apollo/experimental-nextjs-app-support version 0.5.2." + ); + }, + }); } /** @@ -1065,10 +1075,9 @@ export class QueryManager { // Use protected instead of private field so // @apollo/experimental-nextjs-app-support can access type info. - protected inFlightLinkObservables = new Map< - string, - Map> - >(); + protected inFlightLinkObservables = new Trie<{ + observable?: Observable>; + }>(false); private getObservableFromLink( query: DocumentNode, @@ -1078,7 +1087,7 @@ export class QueryManager { deduplication: boolean = context?.queryDeduplication ?? this.queryDeduplication ): Observable> { - let observable: Observable>; + let observable: Observable> | undefined; const { serverQuery, clientQuery } = this.getDocumentInfo(query); if (serverQuery) { @@ -1098,24 +1107,22 @@ export class QueryManager { if (deduplication) { const printedServerQuery = print(serverQuery); - const byVariables = - inFlightLinkObservables.get(printedServerQuery) || new Map(); - inFlightLinkObservables.set(printedServerQuery, byVariables); - const varJson = canonicalStringify(variables); - observable = byVariables.get(varJson); + const entry = inFlightLinkObservables.lookup( + printedServerQuery, + varJson + ); + + observable = entry.observable; if (!observable) { const concast = new Concast([ execute(link, operation) as Observable>, ]); - - byVariables.set(varJson, (observable = concast)); + observable = entry.observable = concast; concast.beforeNext(() => { - if (byVariables.delete(varJson) && byVariables.size < 1) { - inFlightLinkObservables.delete(printedServerQuery); - } + inFlightLinkObservables.remove(printedServerQuery, varJson); }); } } else { diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index 6358d171f4c..8a911fd72de 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -47,6 +47,7 @@ import observableToPromise, { import { itAsync, subscribeAndCount } from "../../../testing/core"; import { ApolloClient } from "../../../core"; import { mockFetchQuery } from "../ObservableQuery"; +import { Concast, print } from "../../../utilities"; interface MockedMutation { reject: (reason: any) => any; @@ -6016,7 +6017,11 @@ describe("QueryManager", () => { queryManager.query({ query, context: { queryDeduplication: true } }); - expect(queryManager["inFlightLinkObservables"].size).toBe(1); + expect( + queryManager["inFlightLinkObservables"].peek(print(query), "{}") + ).toEqual({ + observable: expect.any(Concast), + }); }); it("should allow overriding global queryDeduplication: true to false", () => { @@ -6042,7 +6047,9 @@ describe("QueryManager", () => { queryManager.query({ query, context: { queryDeduplication: false } }); - expect(queryManager["inFlightLinkObservables"].size).toBe(0); + expect( + queryManager["inFlightLinkObservables"].peek(print(query), "{}") + ).toBeUndefined(); }); }); From aaf8c799b517a560245b5c91c33d5699b6c5ef33 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 1 Dec 2023 11:32:44 +0100 Subject: [PATCH 39/90] `useSuspenseQuery`: remove `promiseCache` (#11363) Co-authored-by: Jerel Miller Co-authored-by: jerelmiller Co-authored-by: phryneas --- .api-reports/api-report-react.md | 21 +++++----- .api-reports/api-report-react_hooks.md | 21 +++++----- .api-reports/api-report.md | 21 +++++----- .size-limits.json | 2 +- src/react/cache/QueryReference.ts | 8 ++-- src/react/cache/SuspenseCache.ts | 1 - src/react/cache/types.ts | 4 ++ .../hooks/__tests__/useSuspenseQuery.test.tsx | 3 +- src/react/hooks/useSuspenseQuery.ts | 38 +++++++------------ 9 files changed, 50 insertions(+), 69 deletions(-) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 0b90201df27..4b531996957 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -512,13 +512,6 @@ namespace Cache_2 { import Fragment = DataProxy.Fragment; } -// @public (undocumented) -type CacheKey = [ -query: DocumentNode, -stringifiedVariables: string, -...queryKey: any[] -]; - // @public (undocumented) const enum CacheWriteBehavior { // (undocumented) @@ -916,10 +909,10 @@ class InternalQueryReference { // // (undocumented) fetchMore(options: FetchMoreOptions): Promise>; - // Warning: (ae-forgotten-export) The symbol "CacheKey" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "QueryKey" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly key: CacheKey; + readonly key: QueryKey; // Warning: (ae-forgotten-export) The symbol "Listener" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -929,7 +922,7 @@ class InternalQueryReference { // (undocumented) promise: Promise>; // (undocumented) - promiseCache?: Map>>; + promiseCache?: Map>>; // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) @@ -945,8 +938,6 @@ interface InternalQueryReferenceOptions { // (undocumented) autoDisposeTimeoutMs?: number; // (undocumented) - key: CacheKey; - // (undocumented) onDispose?: () => void; } @@ -1537,6 +1528,12 @@ class QueryInfo { variables?: Record; } +// @public (undocumented) +interface QueryKey { + // (undocumented) + __queryKey?: string; +} + // @public (undocumented) export interface QueryLazyOptions { // (undocumented) diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index 5e17fe31a09..cbeb6aa48a7 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -488,13 +488,6 @@ namespace Cache_2 { import Fragment = DataProxy.Fragment; } -// @public (undocumented) -type CacheKey = [ -query: DocumentNode, -stringifiedVariables: string, -...queryKey: any[] -]; - // @public (undocumented) const enum CacheWriteBehavior { // (undocumented) @@ -863,10 +856,10 @@ class InternalQueryReference { // // (undocumented) fetchMore(options: FetchMoreOptions): Promise>; - // Warning: (ae-forgotten-export) The symbol "CacheKey" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "QueryKey" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly key: CacheKey; + readonly key: QueryKey; // Warning: (ae-forgotten-export) The symbol "Listener" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -876,7 +869,7 @@ class InternalQueryReference { // (undocumented) promise: Promise>; // (undocumented) - promiseCache?: Map>>; + promiseCache?: Map>>; // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) @@ -892,8 +885,6 @@ interface InternalQueryReferenceOptions { // (undocumented) autoDisposeTimeoutMs?: number; // (undocumented) - key: CacheKey; - // (undocumented) onDispose?: () => void; } @@ -1464,6 +1455,12 @@ class QueryInfo { variables?: Record; } +// @public (undocumented) +interface QueryKey { + // (undocumented) + __queryKey?: string; +} + // @public (undocumented) type QueryListener = (queryInfo: QueryInfo) => void; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 04a979da240..1b63551c899 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -502,13 +502,6 @@ class CacheGroup { resetCaching(): void; } -// @public (undocumented) -type CacheKey = [ -query: DocumentNode, -stringifiedVariables: string, -...queryKey: any[] -]; - // @public (undocumented) const enum CacheWriteBehavior { // (undocumented) @@ -1275,10 +1268,10 @@ class InternalQueryReference { // // (undocumented) fetchMore(options: FetchMoreOptions_2): Promise>; - // Warning: (ae-forgotten-export) The symbol "CacheKey" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "QueryKey" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly key: CacheKey; + readonly key: QueryKey; // Warning: (ae-forgotten-export) The symbol "Listener" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1288,7 +1281,7 @@ class InternalQueryReference { // (undocumented) promise: Promise>; // (undocumented) - promiseCache?: Map>>; + promiseCache?: Map>>; // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) @@ -1304,8 +1297,6 @@ interface InternalQueryReferenceOptions { // (undocumented) autoDisposeTimeoutMs?: number; // (undocumented) - key: CacheKey; - // (undocumented) onDispose?: () => void; } @@ -2093,6 +2084,12 @@ class QueryInfo { variables?: Record; } +// @public (undocumented) +interface QueryKey { + // (undocumented) + __queryKey?: string; +} + // @public (undocumented) export interface QueryLazyOptions { // (undocumented) diff --git a/.size-limits.json b/.size-limits.json index dfe8306d94f..dbf215c6ea5 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38693, + "dist/apollo-client.min.cjs": 38675, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32306 } diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 5b2fc14c643..7da866b5309 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -12,7 +12,7 @@ import { createFulfilledPromise, createRejectedPromise, } from "../../utilities/index.js"; -import type { CacheKey } from "./types.js"; +import type { QueryKey } from "./types.js"; import type { useBackgroundQuery, useReadQuery } from "../hooks/index.js"; type Listener = (promise: Promise>) => void; @@ -32,7 +32,6 @@ export interface QueryReference { } interface InternalQueryReferenceOptions { - key: CacheKey; onDispose?: () => void; autoDisposeTimeoutMs?: number; } @@ -65,10 +64,10 @@ type ObservedOptions = Pick< export class InternalQueryReference { public result: ApolloQueryResult; - public readonly key: CacheKey; + public readonly key: QueryKey = {}; public readonly observable: ObservableQuery; - public promiseCache?: Map>>; + public promiseCache?: Map>>; public promise: Promise>; private subscription: ObservableSubscription; @@ -92,7 +91,6 @@ export class InternalQueryReference { // Don't save this result as last result to prevent delivery of last result // when first subscribing this.result = observable.getCurrentResult(false); - this.key = options.key; if (options.onDispose) { this.onDispose = options.onDispose; diff --git a/src/react/cache/SuspenseCache.ts b/src/react/cache/SuspenseCache.ts index af883c0a52a..36641c76cb4 100644 --- a/src/react/cache/SuspenseCache.ts +++ b/src/react/cache/SuspenseCache.ts @@ -38,7 +38,6 @@ export class SuspenseCache { if (!ref.current) { ref.current = new InternalQueryReference(createObservable(), { - key: cacheKey, autoDisposeTimeoutMs: this.options.autoDisposeTimeoutMs, onDispose: () => { delete ref.current; diff --git a/src/react/cache/types.ts b/src/react/cache/types.ts index e9e0ba8b826..40f3c4cc8fc 100644 --- a/src/react/cache/types.ts +++ b/src/react/cache/types.ts @@ -5,3 +5,7 @@ export type CacheKey = [ stringifiedVariables: string, ...queryKey: any[], ]; + +export interface QueryKey { + __queryKey?: string; +} diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index 642be7d023a..86af1878883 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -3650,8 +3650,7 @@ describe("useSuspenseQuery", () => { }); await waitFor(() => expect(renders.errorCount).toBe(1)); - - expect(client.getObservableQueries().size).toBe(0); + await waitFor(() => expect(client.getObservableQueries().size).toBe(0)); }); it('throws network errors when errorPolicy is set to "none"', async () => { diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index ed203d4cfee..8de82bf471e 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -25,7 +25,7 @@ import { getSuspenseCache } from "../cache/index.js"; import { canonicalStringify } from "../../cache/index.js"; import { skipToken } from "./constants.js"; import type { SkipToken } from "./constants.js"; -import type { CacheKey } from "../cache/types.js"; +import type { CacheKey, QueryKey } from "../cache/types.js"; export interface UseSuspenseQueryResult< TData = unknown, @@ -196,29 +196,26 @@ export function useSuspenseQuery< client.watchQuery(watchQueryOptions) ); - const [promiseCache, setPromiseCache] = React.useState( - () => new Map([[queryRef.key, queryRef.promise]]) - ); - - let promise = promiseCache.get(queryRef.key); + let [current, setPromise] = React.useState< + [QueryKey, Promise>] + >([queryRef.key, queryRef.promise]); - if (queryRef.didChangeOptions(watchQueryOptions)) { - promise = queryRef.applyOptions(watchQueryOptions); - promiseCache.set(queryRef.key, promise); + // This saves us a re-execution of the render function when a variable changed. + if (current[0] !== queryRef.key) { + current[0] = queryRef.key; + current[1] = queryRef.promise; } + let promise = current[1]; - if (!promise) { - promise = queryRef.promise; - promiseCache.set(queryRef.key, promise); + if (queryRef.didChangeOptions(watchQueryOptions)) { + current[1] = promise = queryRef.applyOptions(watchQueryOptions); } React.useEffect(() => { const dispose = queryRef.retain(); const removeListener = queryRef.listen((promise) => { - setPromiseCache((promiseCache) => - new Map(promiseCache).set(queryRef.key, promise) - ); + setPromise([queryRef.key, promise]); }); return () => { @@ -239,14 +236,10 @@ export function useSuspenseQuery< }, [queryRef.result]); const result = fetchPolicy === "standby" ? skipResult : __use(promise); - const fetchMore = React.useCallback( ((options) => { const promise = queryRef.fetchMore(options); - - setPromiseCache((previousPromiseCache) => - new Map(previousPromiseCache).set(queryRef.key, queryRef.promise) - ); + setPromise([queryRef.key, queryRef.promise]); return promise; }) satisfies FetchMoreFunction< @@ -259,10 +252,7 @@ export function useSuspenseQuery< const refetch: RefetchFunction = React.useCallback( (variables) => { const promise = queryRef.refetch(variables); - - setPromiseCache((previousPromiseCache) => - new Map(previousPromiseCache).set(queryRef.key, queryRef.promise) - ); + setPromise([queryRef.key, queryRef.promise]); return promise; }, From ea8fd387235b2b4145eb9da0e961e3750317fd7c Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 1 Dec 2023 14:52:01 +0100 Subject: [PATCH 40/90] apply prettier --- src/cache/inmemory/__tests__/client.ts | 30 +++++------ .../hooks/__tests__/useLoadableQuery.test.tsx | 54 ++++++++----------- src/react/hooks/useLoadableQuery.ts | 21 ++++---- src/testing/core/mocking/mockLink.ts | 6 +-- src/testing/internal/profile/profile.tsx | 19 ++++--- src/testing/matchers/index.d.ts | 27 +++++----- 6 files changed, 70 insertions(+), 87 deletions(-) diff --git a/src/cache/inmemory/__tests__/client.ts b/src/cache/inmemory/__tests__/client.ts index c3844cb20c0..23fd87f6f73 100644 --- a/src/cache/inmemory/__tests__/client.ts +++ b/src/cache/inmemory/__tests__/client.ts @@ -90,11 +90,9 @@ describe("InMemoryCache tests exercising ApolloClient", () => { // will remain as a raw string rather than being converted to a Date by // the read function. const expectedDateAfterResult = - fetchPolicy === "cache-only" - ? new Date(dateFromCache) - : fetchPolicy === "no-cache" - ? dateFromNetwork - : new Date(dateFromNetwork); + fetchPolicy === "cache-only" ? new Date(dateFromCache) + : fetchPolicy === "no-cache" ? dateFromNetwork + : new Date(dateFromNetwork); if (adjustedCount === 1) { expect(result.loading).toBe(true); @@ -108,11 +106,11 @@ describe("InMemoryCache tests exercising ApolloClient", () => { // The no-cache fetch policy does return extraneous fields from the // raw network result that were not requested in the query, since // the cache is not consulted. - ...(fetchPolicy === "no-cache" - ? { - ignored: "irrelevant to the subscribed query", - } - : null), + ...(fetchPolicy === "no-cache" ? + { + ignored: "irrelevant to the subscribed query", + } + : null), }); if (fetchPolicy === "no-cache") { @@ -145,12 +143,12 @@ describe("InMemoryCache tests exercising ApolloClient", () => { // network, so it never ends up writing the date field into the // cache explicitly, though Query.date can still be synthesized by // the read function. - ...(fetchPolicy === "cache-only" - ? null - : { - // Make sure this field is stored internally as a raw string. - date: dateFromNetwork, - }), + ...(fetchPolicy === "cache-only" ? null : ( + { + // Make sure this field is stored internally as a raw string. + date: dateFromNetwork, + } + )), // Written explicitly with cache.writeQuery above. missing: "not missing anymore", // The ignored field is never written to the cache, because it is diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index e82dc181f66..cbb72efdf94 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -86,17 +86,15 @@ interface VariablesCaseVariables { } function useVariablesQueryCase() { - const query: TypedDocumentNode< - VariablesCaseData, - VariablesCaseVariables - > = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name + const query: TypedDocumentNode = + gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } } - } - `; + `; const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"]; const mocks: MockedResponse[] = [...CHARACTERS].map( @@ -123,17 +121,15 @@ interface PaginatedQueryVariables { } function usePaginatedQueryCase() { - const query: TypedDocumentNode< - PaginatedQueryData, - PaginatedQueryVariables - > = gql` - query letters($limit: Int, $offset: Int) { - letters(limit: $limit) { - letter - position + const query: TypedDocumentNode = + gql` + query letters($limit: Int, $offset: Int) { + letters(limit: $limit) { + letter + position + } } - } - `; + `; const data = "ABCDEFG" .split("") @@ -170,9 +166,9 @@ function createDefaultProfiledComponents< result: UseReadQueryResult | null; error?: Error | null; }, - TData = Snapshot["result"] extends UseReadQueryResult | null - ? TData - : unknown, + TData = Snapshot["result"] extends UseReadQueryResult | null ? + TData + : unknown, >(profiler: Profiler) { function SuspenseFallback() { useTrackRenders(); @@ -4343,10 +4339,8 @@ describe.skip("type tests", () => { }); it("optional variables are optional to loadQuery", () => { - const query: TypedDocumentNode< - { posts: string[] }, - { limit?: number } - > = gql``; + const query: TypedDocumentNode<{ posts: string[] }, { limit?: number }> = + gql``; const [loadQuery] = useLoadableQuery(query); @@ -4365,10 +4359,8 @@ describe.skip("type tests", () => { }); it("enforces required variables when TVariables includes required variables", () => { - const query: TypedDocumentNode< - { character: string }, - { id: string } - > = gql``; + const query: TypedDocumentNode<{ character: string }, { id: string }> = + gql``; const [loadQuery] = useLoadableQuery(query); diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 2ec551bf26e..771c02afc5f 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -30,11 +30,9 @@ export type LoadQueryFunction = ( // which case we don't want to allow a variables argument. In other // words, we don't want to allow variables to be passed as an argument to this // function if the query does not expect variables in the document. - ...args: [TVariables] extends [never] - ? [] - : {} extends OnlyRequiredProperties - ? [variables?: TVariables] - : [variables: TVariables] + ...args: [TVariables] extends [never] ? [] + : {} extends OnlyRequiredProperties ? [variables?: TVariables] + : [variables: TVariables] ) => void; type ResetFunction = () => void; @@ -60,13 +58,12 @@ export function useLoadableQuery< query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions & TOptions ): UseLoadableQueryResult< - TOptions["errorPolicy"] extends "ignore" | "all" - ? TOptions["returnPartialData"] extends true - ? DeepPartial | undefined - : TData | undefined - : TOptions["returnPartialData"] extends true - ? DeepPartial - : TData, + TOptions["errorPolicy"] extends "ignore" | "all" ? + TOptions["returnPartialData"] extends true ? + DeepPartial | undefined + : TData | undefined + : TOptions["returnPartialData"] extends true ? DeepPartial + : TData, TVariables >; diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index 8d843f5524d..5d09bc7b9ec 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -177,9 +177,9 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} } else { if (response.result) { observer.next( - typeof response.result === "function" - ? response.result(operation.variables) - : response.result + typeof response.result === "function" ? + response.result(operation.variables) + : response.result ); } observer.complete(); diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 79bb2206688..9257ea4f203 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -219,9 +219,8 @@ export function createProfiler({ }); const snapshot = snapshotRef.current as Snapshot; - const domSnapshot = snapshotDOM - ? window.document.body.innerHTML - : undefined; + const domSnapshot = + snapshotDOM ? window.document.body.innerHTML : undefined; const render = new RenderInstance( baseRender, snapshot, @@ -374,13 +373,13 @@ type ResultReplaceRenderWithSnapshot = : T; type ProfiledHookFields = - ProfiledComponentFields extends infer PC - ? { - [K in keyof PC as StringReplaceRenderWithSnapshot< - K & string - >]: ResultReplaceRenderWithSnapshot; - } - : never; + ProfiledComponentFields extends infer PC ? + { + [K in keyof PC as StringReplaceRenderWithSnapshot< + K & string + >]: ResultReplaceRenderWithSnapshot; + } + : never; /** @internal */ export interface ProfiledHook diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 4f6a4de8a5f..690589af128 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -30,23 +30,20 @@ interface ApolloCustomMatchers { ) => R : { error: "matcher needs to be called on an ApolloClient instance" }; - toRerender: T extends - | Profiler - | ProfiledComponent - | ProfiledHook - ? (options?: NextRenderOptions) => Promise - : { error: "matcher needs to be called on a ProfiledComponent instance" }; + toRerender: T extends ( + Profiler | ProfiledComponent | ProfiledHook + ) ? + (options?: NextRenderOptions) => Promise + : { error: "matcher needs to be called on a ProfiledComponent instance" }; - toRenderExactlyTimes: T extends - | Profiler - | ProfiledComponent - | ProfiledHook - ? (count: number, options?: NextRenderOptions) => Promise - : { error: "matcher needs to be called on a ProfiledComponent instance" }; + toRenderExactlyTimes: T extends ( + Profiler | ProfiledComponent | ProfiledHook + ) ? + (count: number, options?: NextRenderOptions) => Promise + : { error: "matcher needs to be called on a ProfiledComponent instance" }; - toBeGarbageCollected: T extends WeakRef - ? () => Promise - : { error: "matcher needs to be called on a WeakRef instance" }; + toBeGarbageCollected: T extends WeakRef ? () => Promise + : { error: "matcher needs to be called on a WeakRef instance" }; } declare global { From b1ff9c28cdc65b1b912d736ff9274bb8ce738a60 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 1 Dec 2023 14:56:12 +0100 Subject: [PATCH 41/90] update api-reports --- .api-reports/api-report-react.md | 2 +- .api-reports/api-report-react_hooks.md | 2 +- .api-reports/api-report.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 8ed6f581504..00e6b5ea122 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -2223,7 +2223,7 @@ interface WatchQueryOptions Date: Fri, 1 Dec 2023 15:20:20 +0100 Subject: [PATCH 42/90] `documentTransform`: use `optimism` and `WeakCache` (#11389) --- .api-reports/api-report-core.md | 5 -- .api-reports/api-report-react.md | 8 +-- .api-reports/api-report-react_components.md | 8 +-- .api-reports/api-report-react_context.md | 8 +-- .api-reports/api-report-react_hoc.md | 8 +-- .api-reports/api-report-react_hooks.md | 8 +-- .api-reports/api-report-react_ssr.md | 8 +-- .api-reports/api-report-testing.md | 8 +-- .api-reports/api-report-testing_core.md | 8 +-- .api-reports/api-report-utilities.md | 5 -- .api-reports/api-report.md | 5 -- .changeset/dirty-kids-crash.md | 5 ++ .size-limits.json | 4 +- src/utilities/graphql/DocumentTransform.ts | 65 ++++++++++----------- 14 files changed, 54 insertions(+), 99 deletions(-) create mode 100644 .changeset/dirty-kids-crash.md diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index ecbbbe9959d..8129823a675 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -558,11 +558,6 @@ export class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; resetCache(): void; // (undocumented) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 00e6b5ea122..16a48677e3e 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -677,11 +677,6 @@ class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; resetCache(): void; // (undocumented) @@ -697,6 +692,8 @@ type DocumentTransformCacheKey = ReadonlyArray; interface DocumentTransformOptions { // (undocumented) cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + // // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -2224,7 +2221,6 @@ interface WatchQueryOptions; interface DocumentTransformOptions { // (undocumented) cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + // // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -1689,7 +1686,6 @@ interface WatchQueryOptions; interface DocumentTransformOptions { // (undocumented) cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + // // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -1585,7 +1582,6 @@ interface WatchQueryOptions; interface DocumentTransformOptions { // (undocumented) cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + // // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -1630,7 +1627,6 @@ export function withSubscription; interface DocumentTransformOptions { // (undocumented) cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + // // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -2115,7 +2112,6 @@ interface WatchQueryOptions; interface DocumentTransformOptions { // (undocumented) cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + // // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -1571,7 +1568,6 @@ interface WatchQueryOptions; interface DocumentTransformOptions { // (undocumented) cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + // // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -1633,7 +1630,6 @@ export function withWarningSpy(it: (...args: TArgs // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:129:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 54169c324b6..a306aeb99fb 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -537,11 +537,6 @@ class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; resetCache(): void; // (undocumented) @@ -557,6 +552,8 @@ type DocumentTransformCacheKey = ReadonlyArray; interface DocumentTransformOptions { // (undocumented) cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + // // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -1590,7 +1587,6 @@ export function withWarningSpy(it: (...args: TArgs // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:129:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 0f3236d629d..ac31c32b9d2 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -715,11 +715,6 @@ export class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; resetCache(): void; // (undocumented) diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index baf0f36ed45..d6b96223bdf 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -701,11 +701,6 @@ export class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; resetCache(): void; // (undocumented) diff --git a/.changeset/dirty-kids-crash.md b/.changeset/dirty-kids-crash.md new file mode 100644 index 00000000000..504c049268d --- /dev/null +++ b/.changeset/dirty-kids-crash.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`documentTransform`: use `optimism` and `WeakCache` instead of directly storing data on the `Trie` diff --git a/.size-limits.json b/.size-limits.json index 9d19488680a..6f8c966652b 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38646, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32304 + "dist/apollo-client.min.cjs": 38632, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32319 } diff --git a/src/utilities/graphql/DocumentTransform.ts b/src/utilities/graphql/DocumentTransform.ts index d3b4870f390..7a5ce40fe4c 100644 --- a/src/utilities/graphql/DocumentTransform.ts +++ b/src/utilities/graphql/DocumentTransform.ts @@ -3,6 +3,8 @@ import { canUseWeakMap, canUseWeakSet } from "../common/canUse.js"; import { checkDocument } from "./getFromAST.js"; import { invariant } from "../globals/index.js"; import type { DocumentNode } from "graphql"; +import { WeakCache } from "@wry/caches"; +import { wrap } from "optimism"; export type DocumentTransformCacheKey = ReadonlyArray; @@ -21,14 +23,11 @@ function identity(document: DocumentNode) { export class DocumentTransform { private readonly transform: TransformFn; + private cached: boolean; private readonly resultCache = canUseWeakSet ? new WeakSet() : new Set(); - private stableCacheKeys: - | Trie<{ key: DocumentTransformCacheKey; value?: DocumentNode }> - | undefined; - // This default implementation of getCacheKey can be overridden by providing // options.getCacheKey to the DocumentTransform constructor. In general, a // getCacheKey function may either return an array of keys (often including @@ -73,18 +72,40 @@ export class DocumentTransform { // Override default `getCacheKey` function, which returns [document]. this.getCacheKey = options.getCacheKey; } + this.cached = options.cache !== false; - if (options.cache !== false) { - this.stableCacheKeys = new Trie(canUseWeakMap, (key) => ({ key })); - } + this.resetCache(); } /** * Resets the internal cache of this transform, if it has one. */ resetCache() { - this.stableCacheKeys = - this.stableCacheKeys && new Trie(canUseWeakMap, (key) => ({ key })); + if (this.cached) { + const stableCacheKeys = new Trie(canUseWeakMap); + this.performWork = wrap( + DocumentTransform.prototype.performWork.bind(this), + { + makeCacheKey: (document) => { + const cacheKeys = this.getCacheKey(document); + if (cacheKeys) { + invariant( + Array.isArray(cacheKeys), + "`getCacheKey` must return an array or undefined" + ); + return stableCacheKeys.lookupArray(cacheKeys); + } + }, + max: 1000 /** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */, + cache: WeakCache, + } + ); + } + } + + private performWork(document: DocumentNode) { + checkDocument(document); + return this.transform(document); } transformDocument(document: DocumentNode) { @@ -94,22 +115,10 @@ export class DocumentTransform { return document; } - const cacheEntry = this.getStableCacheEntry(document); - - if (cacheEntry && cacheEntry.value) { - return cacheEntry.value; - } - - checkDocument(document); - - const transformedDocument = this.transform(document); + const transformedDocument = this.performWork(document); this.resultCache.add(transformedDocument); - if (cacheEntry) { - cacheEntry.value = transformedDocument; - } - return transformedDocument; } @@ -124,16 +133,4 @@ export class DocumentTransform { { cache: false } ); } - - getStableCacheEntry(document: DocumentNode) { - if (!this.stableCacheKeys) return; - const cacheKeys = this.getCacheKey(document); - if (cacheKeys) { - invariant( - Array.isArray(cacheKeys), - "`getCacheKey` must return an array or undefined" - ); - return this.stableCacheKeys.lookupArray(cacheKeys); - } - } } From db5f5fd3a955eb95dc36c4ba7b05f82539e0cd6b Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 1 Dec 2023 19:13:21 +0100 Subject: [PATCH 43/90] `useBackgroundQuery`: remove `promiseCache`, work around race condition (#11366) Co-authored-by: Jerel Miller Co-authored-by: phryneas --- .api-reports/api-report-react.md | 55 +++++++++++-- .api-reports/api-report-react_hooks.md | 55 +++++++++++-- .api-reports/api-report-utilities.md | 4 +- .api-reports/api-report.md | 55 +++++++++++-- .size-limits.json | 4 +- src/react/cache/QueryReference.ts | 69 ++++++++++++----- .../__tests__/useBackgroundQuery.test.tsx | 29 ++++--- .../hooks/__tests__/useLoadableQuery.test.tsx | 77 +++++++++++++++---- src/react/hooks/useBackgroundQuery.ts | 31 ++++---- src/react/hooks/useLoadableQuery.ts | 4 - src/react/hooks/useReadQuery.ts | 27 +++---- src/testing/internal/profile/profile.tsx | 23 +++--- src/utilities/index.ts | 1 + 13 files changed, 313 insertions(+), 121 deletions(-) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 16a48677e3e..99bf23e5739 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -807,6 +807,14 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +interface FulfilledPromise extends Promise { + // (undocumented) + status: "fulfilled"; + // (undocumented) + value: TValue; +} + // @public (undocumented) export function getApolloContext(): ReactTypes.Context; @@ -858,7 +866,7 @@ class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); // (undocumented) - applyOptions(watchQueryOptions: ObservedOptions): Promise>; + applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -877,10 +885,10 @@ class InternalQueryReference { listen(listener: Listener): () => void; // (undocumented) readonly observable: ObservableQuery; + // Warning: (ae-forgotten-export) The symbol "QueryRefPromise" needs to be exported by the entry point index.d.ts + // // (undocumented) - promise: Promise>; - // (undocumented) - promiseCache?: Map>>; + promise: QueryRefPromise; // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) @@ -966,7 +974,7 @@ export type LazyQueryResult = Quer export type LazyQueryResultTuple = [LazyQueryExecFunction, QueryResult]; // @public (undocumented) -type Listener = (promise: Promise>) => void; +type Listener = (promise: QueryRefPromise) => void; // @public (undocumented) export type LoadableQueryHookFetchPolicy = Extract; @@ -1363,9 +1371,25 @@ export namespace parser { // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +interface PendingPromise extends Promise { + // (undocumented) + status: "pending"; +} + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; +// @public (undocumented) +const PROMISE_SYMBOL: unique symbol; + +// Warning: (ae-forgotten-export) The symbol "PendingPromise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "FulfilledPromise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RejectedPromise" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type PromiseWithState = PendingPromise | FulfilledPromise | RejectedPromise; + // @public (undocumented) const QUERY_REFERENCE_SYMBOL: unique symbol; @@ -1619,12 +1643,19 @@ interface QueryOptions { // // @public export interface QueryReference { + // (undocumented) + [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // // (undocumented) - [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; } +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type QueryRefPromise = PromiseWithState>; + // @public (undocumented) export interface QueryResult extends ObservableQueryFields { // (undocumented) @@ -1738,6 +1769,14 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +interface RejectedPromise extends Promise { + // (undocumented) + reason: unknown; + // (undocumented) + status: "rejected"; +} + // @public (undocumented) class RenderPromises { // (undocumented) @@ -2218,8 +2257,8 @@ interface WatchQueryOptions boolean; +// @public (undocumented) +interface FulfilledPromise extends Promise { + // (undocumented) + status: "fulfilled"; + // (undocumented) + value: TValue; +} + // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -805,7 +813,7 @@ class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); // (undocumented) - applyOptions(watchQueryOptions: ObservedOptions): Promise>; + applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -824,10 +832,10 @@ class InternalQueryReference { listen(listener: Listener): () => void; // (undocumented) readonly observable: ObservableQuery; + // Warning: (ae-forgotten-export) The symbol "QueryRefPromise" needs to be exported by the entry point index.d.ts + // // (undocumented) - promise: Promise>; - // (undocumented) - promiseCache?: Map>>; + promise: QueryRefPromise; // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) @@ -917,7 +925,7 @@ interface LazyQueryHookOptions = [LazyQueryExecFunction, QueryResult]; // @public (undocumented) -type Listener = (promise: Promise>) => void; +type Listener = (promise: QueryRefPromise) => void; // @public (undocumented) type LoadableQueryHookFetchPolicy = Extract; @@ -1301,9 +1309,25 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +interface PendingPromise extends Promise { + // (undocumented) + status: "pending"; +} + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; +// @public (undocumented) +const PROMISE_SYMBOL: unique symbol; + +// Warning: (ae-forgotten-export) The symbol "PendingPromise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "FulfilledPromise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RejectedPromise" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type PromiseWithState = PendingPromise | FulfilledPromise | RejectedPromise; + // @public (undocumented) const QUERY_REFERENCE_SYMBOL: unique symbol; @@ -1537,12 +1561,19 @@ interface QueryOptions { // // @public interface QueryReference { + // (undocumented) + [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // // (undocumented) - [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; } +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type QueryRefPromise = PromiseWithState>; + // Warning: (ae-forgotten-export) The symbol "ObservableQueryFields" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -1652,6 +1683,14 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +interface RejectedPromise extends Promise { + // (undocumented) + reason: unknown; + // (undocumented) + status: "rejected"; +} + // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; @@ -2109,8 +2148,8 @@ interface WatchQueryOptions(promise: Promise): promise is PromiseWithState; @@ -1831,7 +1829,7 @@ export { print_2 as print } // Warning: (ae-forgotten-export) The symbol "PendingPromise" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type PromiseWithState = PendingPromise | FulfilledPromise | RejectedPromise; +export type PromiseWithState = PendingPromise | FulfilledPromise | RejectedPromise; // @public (undocumented) class QueryInfo { diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index d6b96223bdf..dd96b473056 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -1025,6 +1025,14 @@ export function fromError(errorValue: any): Observable; // @public (undocumented) export function fromPromise(promise: Promise): Observable; +// @public (undocumented) +interface FulfilledPromise extends Promise { + // (undocumented) + status: "fulfilled"; + // (undocumented) + value: TValue; +} + // @public (undocumented) export function getApolloContext(): ReactTypes.Context; @@ -1204,7 +1212,7 @@ class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); // (undocumented) - applyOptions(watchQueryOptions: ObservedOptions): Promise>; + applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1223,10 +1231,10 @@ class InternalQueryReference { listen(listener: Listener): () => void; // (undocumented) readonly observable: ObservableQuery; + // Warning: (ae-forgotten-export) The symbol "QueryRefPromise" needs to be exported by the entry point index.d.ts + // // (undocumented) - promise: Promise>; - // (undocumented) - promiseCache?: Map>>; + promise: QueryRefPromise; // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) @@ -1358,7 +1366,7 @@ export type LazyQueryResult = Quer export type LazyQueryResultTuple = [LazyQueryExecFunction, QueryResult]; // @public (undocumented) -type Listener = (promise: Promise>) => void; +type Listener = (promise: QueryRefPromise) => void; // @public (undocumented) export type LoadableQueryHookFetchPolicy = Extract; @@ -1844,6 +1852,12 @@ export namespace parser { // @public (undocumented) export type Path = ReadonlyArray; +// @public (undocumented) +interface PendingPromise extends Promise { + // (undocumented) + status: "pending"; +} + // @public (undocumented) class Policies { constructor(config: { @@ -1907,6 +1921,16 @@ interface Printer { (node: ASTNode, originalPrint: typeof print_2): string; } +// @public (undocumented) +const PROMISE_SYMBOL: unique symbol; + +// Warning: (ae-forgotten-export) The symbol "PendingPromise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "FulfilledPromise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RejectedPromise" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type PromiseWithState = PendingPromise | FulfilledPromise | RejectedPromise; + // @public (undocumented) const QUERY_REFERENCE_SYMBOL: unique symbol; @@ -2153,12 +2177,19 @@ export { QueryOptions } // // @public export interface QueryReference { + // (undocumented) + [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // // (undocumented) - [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; } +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type QueryRefPromise = PromiseWithState>; + // @public (undocumented) export interface QueryResult extends ObservableQueryFields { // (undocumented) @@ -2286,6 +2317,14 @@ export type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) export type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +interface RejectedPromise extends Promise { + // (undocumented) + reason: unknown; + // (undocumented) + status: "rejected"; +} + // @public (undocumented) class RenderPromises { // (undocumented) @@ -2862,8 +2901,8 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/QueryManager.ts:395:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:26:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:27:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:31:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useLoadableQuery.ts:49:5 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.size-limits.json b/.size-limits.json index 6f8c966652b..c1d55c13153 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38632, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32319 + "dist/apollo-client.min.cjs": 38618, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32318 } diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 7da866b5309..97025c37335 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -7,28 +7,37 @@ import type { WatchQueryOptions, } from "../../core/index.js"; import { isNetworkRequestSettled } from "../../core/index.js"; -import type { ObservableSubscription } from "../../utilities/index.js"; +import type { + ObservableSubscription, + PromiseWithState, +} from "../../utilities/index.js"; import { createFulfilledPromise, createRejectedPromise, } from "../../utilities/index.js"; import type { QueryKey } from "./types.js"; import type { useBackgroundQuery, useReadQuery } from "../hooks/index.js"; +import { wrapPromiseWithState } from "../../utilities/index.js"; -type Listener = (promise: Promise>) => void; +type QueryRefPromise = PromiseWithState>; + +type Listener = (promise: QueryRefPromise) => void; type FetchMoreOptions = Parameters< ObservableQuery["fetchMore"] >[0]; const QUERY_REFERENCE_SYMBOL: unique symbol = Symbol(); +const PROMISE_SYMBOL: unique symbol = Symbol(); + /** * A `QueryReference` is an opaque object returned by {@link useBackgroundQuery}. * A child component reading the `QueryReference` via {@link useReadQuery} will * suspend until the promise resolves. */ export interface QueryReference { - [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + [PROMISE_SYMBOL]: QueryRefPromise; } interface InternalQueryReferenceOptions { @@ -39,13 +48,34 @@ interface InternalQueryReferenceOptions { export function wrapQueryRef( internalQueryRef: InternalQueryReference ): QueryReference { - return { [QUERY_REFERENCE_SYMBOL]: internalQueryRef }; + return { + [QUERY_REFERENCE_SYMBOL]: internalQueryRef, + [PROMISE_SYMBOL]: internalQueryRef.promise, + }; } export function unwrapQueryRef( queryRef: QueryReference -): InternalQueryReference { - return queryRef[QUERY_REFERENCE_SYMBOL]; +): [InternalQueryReference, () => QueryRefPromise] { + const internalQueryRef = queryRef[QUERY_REFERENCE_SYMBOL]; + + return [ + internalQueryRef, + () => + // There is a chance the query ref's promise has been updated in the time + // the original promise had been suspended. In that case, we want to use + // it instead of the older promise which may contain outdated data. + internalQueryRef.promise.status === "fulfilled" ? + internalQueryRef.promise + : queryRef[PROMISE_SYMBOL], + ]; +} + +export function updateWrappedQueryRef( + queryRef: QueryReference, + promise: QueryRefPromise +) { + queryRef[PROMISE_SYMBOL] = promise; } const OBSERVED_CHANGED_OPTIONS = [ @@ -67,8 +97,7 @@ export class InternalQueryReference { public readonly key: QueryKey = {}; public readonly observable: ObservableQuery; - public promiseCache?: Map>>; - public promise: Promise>; + public promise: QueryRefPromise; private subscription: ObservableSubscription; private listeners = new Set>(); @@ -104,10 +133,12 @@ export class InternalQueryReference { this.promise = createFulfilledPromise(this.result); this.status = "idle"; } else { - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); + this.promise = wrapPromiseWithState( + new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }) + ); } this.subscription = observable @@ -268,23 +299,25 @@ export class InternalQueryReference { break; } case "idle": { - this.promise = createRejectedPromise(error); + this.promise = createRejectedPromise>(error); this.deliver(this.promise); } } } - private deliver(promise: Promise>) { + private deliver(promise: QueryRefPromise) { this.listeners.forEach((listener) => listener(promise)); } private initiateFetch(returnedPromise: Promise>) { this.status = "loading"; - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); + this.promise = wrapPromiseWithState( + new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }) + ); this.promise.catch(() => {}); diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 32bd0edcdc0..85b857e47a5 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -216,6 +216,7 @@ function renderVariablesIntegrationTest({ }, }, }, + delay: 200, }; } ); @@ -642,7 +643,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef).promise; + const _result = await unwrapQueryRef(queryRef)[0].promise; expect(_result).toEqual({ data: { hello: "world 1" }, @@ -679,7 +680,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef).promise; + const _result = await unwrapQueryRef(queryRef)[0].promise; await waitFor(() => { expect(_result).toEqual({ @@ -720,7 +721,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef).promise; + const _result = await unwrapQueryRef(queryRef)[0].promise; await waitFor(() => { expect(_result).toMatchObject({ @@ -780,7 +781,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef).promise; + const _result = await unwrapQueryRef(queryRef)[0].promise; const resultSet = new Set(_result.data.results); const values = Array.from(resultSet).map((item) => item.value); @@ -841,7 +842,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef).promise; + const _result = await unwrapQueryRef(queryRef)[0].promise; const resultSet = new Set(_result.data.results); const values = Array.from(resultSet).map((item) => item.value); @@ -883,7 +884,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef).promise; + const _result = await unwrapQueryRef(queryRef)[0].promise; expect(_result).toEqual({ data: { hello: "from link" }, @@ -923,7 +924,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef).promise; + const _result = await unwrapQueryRef(queryRef)[0].promise; expect(_result).toEqual({ data: { hello: "from cache" }, @@ -970,7 +971,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef).promise; + const _result = await unwrapQueryRef(queryRef)[0].promise; expect(_result).toEqual({ data: { foo: "bar", hello: "from link" }, @@ -1010,7 +1011,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef).promise; + const _result = await unwrapQueryRef(queryRef)[0].promise; expect(_result).toEqual({ data: { hello: "from link" }, @@ -1053,7 +1054,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef).promise; + const _result = await unwrapQueryRef(queryRef)[0].promise; expect(_result).toEqual({ data: { hello: "from link" }, @@ -3224,12 +3225,14 @@ describe("useBackgroundQuery", () => { result: { data: { character: { id: "1", name: "Captain Marvel" } }, }, + delay: 200, }, { request: { query, variables: { id: "2" } }, result: { data: { character: { id: "2", name: "Captain America" } }, }, + delay: 200, }, ]; @@ -3294,7 +3297,7 @@ describe("useBackgroundQuery", () => { // parent component re-suspends expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(2); + expect(renders.count).toBe(1); expect( await screen.findByText("1 - Spider-Man (updated)") @@ -3304,11 +3307,13 @@ describe("useBackgroundQuery", () => { // parent component re-suspends expect(renders.suspenseCount).toBe(3); - expect(renders.count).toBe(3); + expect(renders.count).toBe(2); expect( await screen.findByText("1 - Spider-Man (updated again)") ).toBeInTheDocument(); + + expect(renders.count).toBe(3); }); it("throws errors when errors are returned after calling `refetch`", async () => { using _consoleSpy = spyOnConsole("error"); diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index cbb72efdf94..8bb0a37b2b9 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -158,6 +158,7 @@ function createDefaultProfiler() { error: null as Error | null, result: null as UseReadQueryResult | null, }, + skipNonTrackingRenders: true, }); } @@ -493,16 +494,24 @@ it("allows the client to be overridden", async () => { const { query } = useSimpleQueryCase(); const globalClient = new ApolloClient({ - link: new ApolloLink(() => - Observable.of({ data: { greeting: "global hello" } }) - ), + link: new MockLink([ + { + request: { query }, + result: { data: { greeting: "global hello" } }, + delay: 10, + }, + ]), cache: new InMemoryCache(), }); const localClient = new ApolloClient({ - link: new ApolloLink(() => - Observable.of({ data: { greeting: "local hello" } }) - ), + link: new MockLink([ + { + request: { query }, + result: { data: { greeting: "local hello" } }, + delay: 10, + }, + ]), cache: new InMemoryCache(), }); @@ -512,6 +521,7 @@ it("allows the client to be overridden", async () => { createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query, { client: localClient, }); @@ -562,9 +572,10 @@ it("passes context to the link", async () => { link: new ApolloLink((operation) => { return new Observable((observer) => { const { valueA, valueB } = operation.getContext(); - - observer.next({ data: { context: { valueA, valueB } } }); - observer.complete(); + setTimeout(() => { + observer.next({ data: { context: { valueA, valueB } } }); + observer.complete(); + }, 10); }); }), }); @@ -575,6 +586,7 @@ it("passes context to the link", async () => { createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query, { context: { valueA: "A", valueB: "B" }, }); @@ -657,6 +669,7 @@ it('enables canonical results when canonizeResults is "true"', async () => { createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query, { canonizeResults: true, }); @@ -738,6 +751,7 @@ it("can disable canonical results when the cache's canonizeResults setting is tr createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [loadQuery, queryRef] = useLoadableQuery(query, { canonizeResults: false, }); @@ -1456,12 +1470,14 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async { request: { query }, result: { data: { greeting: "Hello" } }, + delay: 10, }, { request: { query }, result: { errors: [new GraphQLError("oops")], }, + delay: 10, }, ]; @@ -1470,6 +1486,7 @@ it("applies `errorPolicy` on next fetch when it changes between renders", async createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [errorPolicy, setErrorPolicy] = useState("none"); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { errorPolicy, @@ -1541,10 +1558,15 @@ it("applies `context` on next fetch when it changes between renders", async () = `; const link = new ApolloLink((operation) => { - return Observable.of({ - data: { - phase: operation.getContext().phase, - }, + return new Observable((subscriber) => { + setTimeout(() => { + subscriber.next({ + data: { + phase: operation.getContext().phase, + }, + }); + subscriber.complete(); + }, 10); }); }); @@ -1558,6 +1580,7 @@ it("applies `context` on next fetch when it changes between renders", async () = createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [phase, setPhase] = React.useState("initial"); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { context: { phase }, @@ -1659,6 +1682,7 @@ it("returns canonical results immediately when `canonizeResults` changes from `f createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [canonizeResults, setCanonizeResults] = React.useState(false); const [loadQuery, queryRef] = useLoadableQuery(query, { canonizeResults, @@ -1724,6 +1748,7 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren { request: { query, variables: { min: 0, max: 12 } }, result: { data: { primes: [2, 3, 5, 7, 11] } }, + delay: 10, }, { request: { query, variables: { min: 12, max: 30 } }, @@ -1765,6 +1790,7 @@ it("applies changed `refetchWritePolicy` to next fetch when changing between ren createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [refetchWritePolicy, setRefetchWritePolicy] = React.useState("merge"); @@ -1895,6 +1921,7 @@ it("applies `returnPartialData` on next fetch when it changes between renders", }, }, }, + delay: 10, }, { request: { query: fullQuery }, @@ -2067,6 +2094,7 @@ it("applies updated `fetchPolicy` on next fetch when it changes between renders" createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [fetchPolicy, setFetchPolicy] = React.useState("cache-first"); @@ -2237,12 +2265,14 @@ it("re-suspends when calling `refetch` with new variables", async () => { result: { data: { character: { id: "1", name: "Captain Marvel" } }, }, + delay: 10, }, { request: { query, variables: { id: "2" } }, result: { data: { character: { id: "2", name: "Captain America" } }, }, + delay: 10, }, ]; @@ -2319,6 +2349,7 @@ it("re-suspends multiple times when calling `refetch` multiple times", async () data: { character: { id: "1", name: "Spider-Man" } }, }, maxUsageCount: 3, + delay: 10, }, ]; @@ -2434,6 +2465,7 @@ it("throws errors when errors are returned after calling `refetch`", async () => createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( @@ -2491,12 +2523,14 @@ it('ignores errors returned after calling `refetch` when errorPolicy is set to " result: { data: { character: { id: "1", name: "Captain Marvel" } }, }, + delay: 10, }, { request: { query, variables: { id: "1" } }, result: { errors: [new GraphQLError("Something went wrong")], }, + delay: 10, }, ]; @@ -2506,6 +2540,7 @@ it('ignores errors returned after calling `refetch` when errorPolicy is set to " createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { errorPolicy: "ignore", }); @@ -2586,6 +2621,7 @@ it('returns errors after calling `refetch` when errorPolicy is set to "all"', as createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { errorPolicy: "all", }); @@ -2668,6 +2704,7 @@ it('handles partial data results after calling `refetch` when errorPolicy is set createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { errorPolicy: "all", }); @@ -2933,6 +2970,7 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { createDefaultProfiledComponents(Profiler); function App() { + useTrackRenders(); const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); return ( @@ -3000,6 +3038,9 @@ it("properly uses `updateQuery` when calling `fetchMore`", async () => { }); } + // TODO investigate: this test highlights a React render + // that actually doesn't rerender any user-provided components + // so we need to use `skipNonTrackingRenders` await expect(Profiler).not.toRerender(); }); @@ -3023,6 +3064,7 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ }); function App() { + useTrackRenders(); const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); return ( @@ -3083,6 +3125,9 @@ it("properly uses cache field policies when calling `fetchMore` without `updateQ }); } + // TODO investigate: this test highlights a React render + // that actually doesn't rerender any user-provided components + // so we need to use `skipNonTrackingRenders` await expect(Profiler).not.toRerender(); }); @@ -3269,6 +3314,7 @@ it('honors refetchWritePolicy set to "merge"', async () => { { request: { query, variables: { min: 0, max: 12 } }, result: { data: { primes: [2, 3, 5, 7, 11] } }, + delay: 10, }, { request: { query, variables: { min: 12, max: 30 } }, @@ -3304,6 +3350,7 @@ it('honors refetchWritePolicy set to "merge"', async () => { }); function App() { + useTrackRenders(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { refetchWritePolicy: "merge", }); @@ -3381,6 +3428,7 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { { request: { query, variables: { min: 0, max: 12 } }, result: { data: { primes: [2, 3, 5, 7, 11] } }, + delay: 10, }, { request: { query, variables: { min: 12, max: 30 } }, @@ -3416,6 +3464,7 @@ it('defaults refetchWritePolicy to "overwrite"', async () => { }); function App() { + useTrackRenders(); const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); return ( @@ -3691,6 +3740,7 @@ it('suspends when partial data is in the cache and using a "network-only" fetch { request: { query: fullQuery }, result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + delay: 10, }, ]; @@ -3782,6 +3832,7 @@ it('suspends when partial data is in the cache and using a "no-cache" fetch poli { request: { query: fullQuery }, result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + delay: 10, }, ]; diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index d6d4af2789b..47484ad45b8 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -7,7 +7,11 @@ import type { WatchQueryOptions, } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; -import { wrapQueryRef } from "../cache/QueryReference.js"; +import { + unwrapQueryRef, + updateWrappedQueryRef, + wrapQueryRef, +} from "../cache/QueryReference.js"; import type { QueryReference } from "../cache/QueryReference.js"; import type { BackgroundQueryHookOptions, NoInfer } from "../types/types.js"; import { __use } from "./internal/index.js"; @@ -201,13 +205,15 @@ export function useBackgroundQuery< client.watchQuery(watchQueryOptions as WatchQueryOptions) ); - const [promiseCache, setPromiseCache] = React.useState( - () => new Map([[queryRef.key, queryRef.promise]]) + const [wrappedQueryRef, setWrappedQueryRef] = React.useState( + wrapQueryRef(queryRef) ); - + if (unwrapQueryRef(wrappedQueryRef)[0] !== queryRef) { + setWrappedQueryRef(wrapQueryRef(queryRef)); + } if (queryRef.didChangeOptions(watchQueryOptions)) { const promise = queryRef.applyOptions(watchQueryOptions); - promiseCache.set(queryRef.key, promise); + updateWrappedQueryRef(wrappedQueryRef, promise); } React.useEffect(() => queryRef.retain(), [queryRef]); @@ -216,9 +222,7 @@ export function useBackgroundQuery< (options) => { const promise = queryRef.fetchMore(options as FetchMoreQueryOptions); - setPromiseCache((promiseCache) => - new Map(promiseCache).set(queryRef.key, queryRef.promise) - ); + setWrappedQueryRef(wrapQueryRef(queryRef)); return promise; }, @@ -229,22 +233,13 @@ export function useBackgroundQuery< (variables) => { const promise = queryRef.refetch(variables); - setPromiseCache((promiseCache) => - new Map(promiseCache).set(queryRef.key, queryRef.promise) - ); + setWrappedQueryRef(wrapQueryRef(queryRef)); return promise; }, [queryRef] ); - queryRef.promiseCache = promiseCache; - - const wrappedQueryRef = React.useMemo( - () => wrapQueryRef(queryRef), - [queryRef] - ); - return [ didFetchResult.current ? wrappedQueryRef : void 0, { fetchMore, refetch }, diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 771c02afc5f..558479c52f6 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -130,10 +130,6 @@ export function useLoadableQuery< promiseCache.set(queryRef.key, promise); } - if (queryRef) { - queryRef.promiseCache = promiseCache; - } - const calledDuringRender = useRenderGuard(); React.useEffect(() => queryRef?.retain(), [queryRef]); diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 803535c4878..f2320aa58ea 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -1,9 +1,11 @@ import * as React from "rehackt"; -import { unwrapQueryRef } from "../cache/QueryReference.js"; +import { + unwrapQueryRef, + updateWrappedQueryRef, +} from "../cache/QueryReference.js"; import type { QueryReference } from "../cache/QueryReference.js"; import { __use } from "./internal/index.js"; import { toApolloError } from "./useSuspenseQuery.js"; -import { invariant } from "../../utilities/globals/index.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import type { ApolloError } from "../../errors/index.js"; import type { NetworkStatus } from "../../core/index.js"; @@ -36,32 +38,23 @@ export interface UseReadQueryResult { export function useReadQuery( queryRef: QueryReference ): UseReadQueryResult { - const internalQueryRef = unwrapQueryRef(queryRef); - invariant( - internalQueryRef.promiseCache, - "It appears that `useReadQuery` was used outside of `useBackgroundQuery`. " + - "`useReadQuery` is only supported for use with `useBackgroundQuery`. " + - "Please ensure you are passing the `queryRef` returned from `useBackgroundQuery`." + const [internalQueryRef, getPromise] = React.useMemo( + () => unwrapQueryRef(queryRef), + [queryRef] ); - const { promiseCache, key } = internalQueryRef; - - if (!promiseCache.has(key)) { - promiseCache.set(key, internalQueryRef.promise); - } - const promise = useSyncExternalStore( React.useCallback( (forceUpdate) => { return internalQueryRef.listen((promise) => { - internalQueryRef.promiseCache!.set(internalQueryRef.key, promise); + updateWrappedQueryRef(queryRef, promise); forceUpdate(); }); }, [internalQueryRef] ), - () => promiseCache.get(key)!, - () => promiseCache.get(key)! + getPromise, + getPromise ); const result = __use(promise); diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 9257ea4f203..12e681ad7d2 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -98,17 +98,8 @@ export interface ProfiledComponent export function profile({ Component, ...options -}: { - onRender?: ( - info: BaseRender & { - snapshot: Snapshot; - replaceSnapshot: ReplaceSnapshot; - mergeSnapshot: MergeSnapshot; - } - ) => void; +}: Parameters>[0] & { Component: React.ComponentType; - snapshotDOM?: boolean; - initialSnapshot?: Snapshot; }): ProfiledComponent { const Profiler = createProfiler(options); @@ -140,6 +131,7 @@ export function createProfiler({ onRender, snapshotDOM = false, initialSnapshot, + skipNonTrackingRenders, }: { onRender?: ( info: BaseRender & { @@ -150,6 +142,11 @@ export function createProfiler({ ) => void; snapshotDOM?: boolean; initialSnapshot?: Snapshot; + /** + * This will skip renders during which no renders tracked by + * `useTrackRenders` occured. + */ + skipNonTrackingRenders?: boolean; } = {}) { let nextRender: Promise> | undefined; let resolveNextRender: ((render: Render) => void) | undefined; @@ -194,6 +191,12 @@ export function createProfiler({ startTime, commitTime ) => { + if ( + skipNonTrackingRenders && + profilerContext.renderedComponents.length === 0 + ) { + return; + } const baseRender = { id, phase, diff --git a/src/utilities/index.ts b/src/utilities/index.ts index ec05d2aa043..35c1ec45cad 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -98,6 +98,7 @@ export type { } from "./observables/Observable.js"; export { Observable } from "./observables/Observable.js"; +export type { PromiseWithState } from "./promises/decoration.js"; export { isStatefulPromise, createFulfilledPromise, From ac58ca0cdd08e251aab45438dcaaff9f3990ce4e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 02:26:43 -0700 Subject: [PATCH 44/90] Update `useLoadableQuery` to remove `promiseCache` (#11402) Co-authored-by: Lenz Weber-Tronic Co-authored-by: phryneas --- .api-reports/api-report-react.md | 2 +- .api-reports/api-report-react_hooks.md | 2 +- .api-reports/api-report.md | 2 +- .size-limits.json | 2 +- .../hooks/__tests__/useLoadableQuery.test.tsx | 55 ++++++++++++++++ src/react/hooks/useLoadableQuery.ts | 66 +++++++------------ 6 files changed, 83 insertions(+), 46 deletions(-) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 99bf23e5739..268f955f1bf 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -2259,7 +2259,7 @@ interface WatchQueryOptions { + const { query, mocks } = useSimpleQueryCase(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user, unmount } = renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + unmount(); + + // We need to wait a tick since the cleanup is run in a setTimeout to + // prevent strict mode bugs. + await wait(0); + + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + it("changes variables on a query and resuspends when passing new variables to the loadQuery function", async () => { const { query, mocks } = useVariablesQueryCase(); diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 558479c52f6..282988d8a16 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -7,11 +7,12 @@ import type { WatchQueryOptions, } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; -import { wrapQueryRef } from "../cache/QueryReference.js"; -import type { - QueryReference, - InternalQueryReference, +import { + unwrapQueryRef, + updateWrappedQueryRef, + wrapQueryRef, } from "../cache/QueryReference.js"; +import type { QueryReference } from "../cache/QueryReference.js"; import type { LoadableQueryHookOptions } from "../types/types.js"; import { __use, useRenderGuard } from "./internal/index.js"; import { getSuspenseCache } from "../cache/index.js"; @@ -118,60 +119,55 @@ export function useLoadableQuery< const watchQueryOptions = useWatchQueryOptions({ client, query, options }); const { queryKey = [] } = options; - const [queryRef, setQueryRef] = - React.useState | null>(null); - - const [promiseCache, setPromiseCache] = React.useState(() => - queryRef ? new Map([[queryRef.key, queryRef.promise]]) : new Map() + const [queryRef, setQueryRef] = React.useState | null>( + null ); - if (queryRef?.didChangeOptions(watchQueryOptions)) { - const promise = queryRef.applyOptions(watchQueryOptions); - promiseCache.set(queryRef.key, promise); + const internalQueryRef = queryRef && unwrapQueryRef(queryRef)[0]; + + if (queryRef && internalQueryRef?.didChangeOptions(watchQueryOptions)) { + const promise = internalQueryRef.applyOptions(watchQueryOptions); + updateWrappedQueryRef(queryRef, promise); } const calledDuringRender = useRenderGuard(); - React.useEffect(() => queryRef?.retain(), [queryRef]); + React.useEffect(() => internalQueryRef?.retain(), [internalQueryRef]); const fetchMore: FetchMoreFunction = React.useCallback( (options) => { - if (!queryRef) { + if (!internalQueryRef) { throw new Error( "The query has not been loaded. Please load the query." ); } - const promise = queryRef.fetchMore( + const promise = internalQueryRef.fetchMore( options as FetchMoreQueryOptions ); - setPromiseCache((promiseCache) => - new Map(promiseCache).set(queryRef.key, queryRef.promise) - ); + setQueryRef(wrapQueryRef(internalQueryRef)); return promise; }, - [queryRef] + [internalQueryRef] ); const refetch: RefetchFunction = React.useCallback( (options) => { - if (!queryRef) { + if (!internalQueryRef) { throw new Error( "The query has not been loaded. Please load the query." ); } - const promise = queryRef.refetch(options); + const promise = internalQueryRef.refetch(options); - setPromiseCache((promiseCache) => - new Map(promiseCache).set(queryRef.key, queryRef.promise) - ); + setQueryRef(wrapQueryRef(internalQueryRef)); return promise; }, - [queryRef] + [internalQueryRef] ); const loadQuery: LoadQueryFunction = React.useCallback( @@ -196,28 +192,14 @@ export function useLoadableQuery< } as WatchQueryOptions) ); - promiseCache.set(queryRef.key, queryRef.promise); - setQueryRef(queryRef); + setQueryRef(wrapQueryRef(queryRef)); }, - [ - query, - queryKey, - suspenseCache, - watchQueryOptions, - promiseCache, - calledDuringRender, - ] + [query, queryKey, suspenseCache, watchQueryOptions, calledDuringRender] ); const reset: ResetFunction = React.useCallback(() => { setQueryRef(null); }, [queryRef]); - return React.useMemo(() => { - return [ - loadQuery, - queryRef && wrapQueryRef(queryRef), - { fetchMore, refetch, reset }, - ]; - }, [queryRef, loadQuery, fetchMore, refetch, reset]); + return [loadQuery, queryRef, { fetchMore, refetch, reset }]; } From 838095be8b3346ea9bd18c6e46db3193e6454285 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 4 Dec 2023 17:18:01 -0700 Subject: [PATCH 45/90] Version Packages (alpha) (#11353) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 13 +++++++++- CHANGELOG.md | 60 +++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 +-- package.json | 2 +- 4 files changed, 75 insertions(+), 4 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 87706b33f54..f51bce71873 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -7,11 +7,22 @@ "changesets": [ "beige-geese-wink", "breezy-spiders-tap", + "clean-items-smash", + "cold-llamas-turn", + "dirty-kids-crash", + "forty-cups-shop", "friendly-clouds-laugh", - "good-experts-repair", + "hot-ducks-burn", + "polite-avocados-warn", + "quick-hats-marry", "shaggy-ears-scream", + "shaggy-sheep-pull", + "sixty-boxes-rest", "sour-sheep-walk", "strong-terms-perform", + "thick-mice-collect", + "thirty-ties-arrive", + "violet-lions-draw", "wild-dolphins-jog", "yellow-flies-repeat" ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 43ee4c79a21..c7326ece2c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,64 @@ # @apollo/client +## 3.9.0-alpha.5 + +### Minor Changes + +- [#11345](https://github.com/apollographql/apollo-client/pull/11345) [`1759066a8`](https://github.com/apollographql/apollo-client/commit/1759066a8f9a204e49228568aef9446a64890ff3) Thanks [@phryneas](https://github.com/phryneas)! - `QueryManager.inFlightLinkObservables` now uses a strong `Trie` as an internal data structure. + + #### Warning: requires `@apollo/experimental-nextjs-app-support` update + + If you are using `@apollo/experimental-nextjs-app-support`, you will need to update that to at least 0.5.2, as it accesses this internal data structure. + +- [#11300](https://github.com/apollographql/apollo-client/pull/11300) [`a8158733c`](https://github.com/apollographql/apollo-client/commit/a8158733cfa3e65180ec23518d657ea41894bb2b) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Introduces a new `useLoadableQuery` hook. This hook works similarly to `useBackgroundQuery` in that it returns a `queryRef` that can be used to suspend a component via the `useReadQuery` hook. It provides a more ergonomic way to load the query during a user interaction (for example when wanting to preload some data) that would otherwise be clunky with `useBackgroundQuery`. + + ```tsx + function App() { + const [loadQuery, queryRef, { refetch, fetchMore, reset }] = + useLoadableQuery(query, options); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Child({ queryRef }) { + const { data } = useReadQuery(queryRef); + + // ... + } + ``` + +### Patch Changes + +- [#11356](https://github.com/apollographql/apollo-client/pull/11356) [`cc4ac7e19`](https://github.com/apollographql/apollo-client/commit/cc4ac7e1917f046bcd177882727864eed40b910e) Thanks [@phryneas](https://github.com/phryneas)! - Fix a potential memory leak in `FragmentRegistry.transform` and `FragmentRegistry.findFragmentSpreads` that would hold on to passed-in `DocumentNodes` for too long. + +- [#11370](https://github.com/apollographql/apollo-client/pull/11370) [`25e2cb431`](https://github.com/apollographql/apollo-client/commit/25e2cb431c76ec5aa88202eaacbd98fad42edc7f) Thanks [@phryneas](https://github.com/phryneas)! - `parse` function: improve memory management + + - use LRU `WeakCache` instead of `Map` to keep a limited number of parsed results + - cache is initiated lazily, only when needed + - expose `parse.resetCache()` method + +- [#11389](https://github.com/apollographql/apollo-client/pull/11389) [`139acd115`](https://github.com/apollographql/apollo-client/commit/139acd1153afa1445b69dcb4e139668ab8c5889a) Thanks [@phryneas](https://github.com/phryneas)! - `documentTransform`: use `optimism` and `WeakCache` instead of directly storing data on the `Trie` + +- [#11358](https://github.com/apollographql/apollo-client/pull/11358) [`7d939f80f`](https://github.com/apollographql/apollo-client/commit/7d939f80fbc2c419c58a6c55b6a35ee7474d0379) Thanks [@phryneas](https://github.com/phryneas)! - Fixes a potential memory leak in `Concast` that might have been triggered when `Concast` was used outside of Apollo Client. + +- [#11344](https://github.com/apollographql/apollo-client/pull/11344) [`bd2667619`](https://github.com/apollographql/apollo-client/commit/bd2667619700139af32a45364794d11f845ab6cf) Thanks [@phryneas](https://github.com/phryneas)! - Add a `resetCache` method to `DocumentTransform` and hook `InMemoryCache.addTypenameTransform` up to `InMemoryCache.gc` + +- [#11367](https://github.com/apollographql/apollo-client/pull/11367) [`30d17bfeb`](https://github.com/apollographql/apollo-client/commit/30d17bfebe44dbfa7b78c8982cfeb49afd37129c) Thanks [@phryneas](https://github.com/phryneas)! - `print`: use `WeakCache` instead of `WeakMap` + +- [#11385](https://github.com/apollographql/apollo-client/pull/11385) [`d9ca4f082`](https://github.com/apollographql/apollo-client/commit/d9ca4f0821c66ae4f03cf35a7ac93fe604cc6de3) Thanks [@phryneas](https://github.com/phryneas)! - ensure `defaultContext` is also used for mutations and subscriptions + +- [#11387](https://github.com/apollographql/apollo-client/pull/11387) [`4dce8673b`](https://github.com/apollographql/apollo-client/commit/4dce8673b1757d8a3a4edd2996d780e86fad14e3) Thanks [@phryneas](https://github.com/phryneas)! - `QueryManager.transformCache`: use `WeakCache` instead of `WeakMap` + +- [#11371](https://github.com/apollographql/apollo-client/pull/11371) [`ebd8fe2c1`](https://github.com/apollographql/apollo-client/commit/ebd8fe2c1b8b50bfeb2da20aeca5671300fb5564) Thanks [@phryneas](https://github.com/phryneas)! - Clarify types of `EntityStore.makeCacheKey`. + +- [#11355](https://github.com/apollographql/apollo-client/pull/11355) [`7d8e18493`](https://github.com/apollographql/apollo-client/commit/7d8e18493cd13134726c6643cbf0fadb08be2d37) Thanks [@phryneas](https://github.com/phryneas)! - InMemoryCache.gc now also triggers FragmentRegistry.resetCaches (if there is a FragmentRegistry) ## 3.8.8 @@ -128,6 +187,7 @@ - [#6701](https://github.com/apollographql/apollo-client/pull/6701) [`8d2b4e107`](https://github.com/apollographql/apollo-client/commit/8d2b4e107d7c21563894ced3a65d631183b58fd9) Thanks [@prowe](https://github.com/prowe)! - Ability to dynamically match mocks Adds support for a new property `MockedResponse.variableMatcher`: a predicate function that accepts a `variables` param. If `true`, the `variables` will be passed into the `ResultFunction` to help dynamically build a response. + ## 3.8.7 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 1daf4c7fcd1..71a75840fdf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.4", + "version": "3.9.0-alpha.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.0-alpha.4", + "version": "3.9.0-alpha.5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 69a308a4805..dc9e6941396 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.4", + "version": "3.9.0-alpha.5", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 3f7eecbfbd4f4444cffcaac7dd9fd225c8c2a401 Mon Sep 17 00:00:00 2001 From: Aditya Kumawat Date: Thu, 7 Dec 2023 00:40:20 +0530 Subject: [PATCH 46/90] Add `skipPollAttempt` option to control polling refetch behavior (#11397) Co-authored-by: Aditya Kumawat --- .api-reports/api-report-core.md | 3 +- .api-reports/api-report-react.md | 3 +- .api-reports/api-report-react_components.md | 3 +- .api-reports/api-report-react_context.md | 3 +- .api-reports/api-report-react_hoc.md | 3 +- .api-reports/api-report-react_hooks.md | 3 +- .api-reports/api-report-react_ssr.md | 3 +- .api-reports/api-report-testing.md | 3 +- .api-reports/api-report-testing_core.md | 3 +- .api-reports/api-report-utilities.md | 3 +- .api-reports/api-report.md | 3 +- .changeset/swift-zoos-collect.md | 19 ++ .size-limits.json | 4 +- src/core/ObservableQuery.ts | 5 +- src/core/watchQueryOptions.ts | 7 + src/react/hooks/__tests__/useQuery.test.tsx | 194 ++++++++++++++++++++ 16 files changed, 248 insertions(+), 14 deletions(-) create mode 100644 .changeset/swift-zoos-collect.md diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 8129823a675..e16b21db97b 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -2069,6 +2069,7 @@ export interface WatchQueryOptions; refetchWritePolicy?: RefetchWritePolicy; returnPartialData?: boolean; + skipPollAttempt?: () => boolean; variables?: TVariables; } @@ -2115,7 +2116,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:395:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 268f955f1bf..7acf9ba2c5a 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -2235,6 +2235,7 @@ interface WatchQueryOptions boolean; variables?: TVariables; } @@ -2256,7 +2257,7 @@ interface WatchQueryOptions boolean; variables?: TVariables; } @@ -1685,7 +1686,7 @@ interface WatchQueryOptions boolean; variables?: TVariables; } @@ -1581,7 +1582,7 @@ interface WatchQueryOptions boolean; variables?: TVariables; } @@ -1626,7 +1627,7 @@ export function withSubscription boolean; variables?: TVariables; } @@ -2147,7 +2148,7 @@ interface WatchQueryOptions boolean; variables?: TVariables; } @@ -1567,7 +1568,7 @@ interface WatchQueryOptions boolean; variables?: TVariables; } @@ -1629,7 +1630,7 @@ export function withWarningSpy(it: (...args: TArgs // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index a306aeb99fb..ad2f42b7825 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -1556,6 +1556,7 @@ interface WatchQueryOptions boolean; variables?: TVariables; } @@ -1586,7 +1587,7 @@ export function withWarningSpy(it: (...args: TArgs // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 2c4231f2096..9ccfda99640 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -2413,6 +2413,7 @@ interface WatchQueryOptions boolean; variables?: TVariables; } @@ -2469,7 +2470,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/utilities/graphql/storeUtils.ts:226:12 - (ae-forgotten-export) The symbol "storeKeyNameStringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 081d7af9dc9..5a22a0eefdd 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -2853,6 +2853,7 @@ export interface WatchQueryOptions; refetchWritePolicy?: RefetchWritePolicy; returnPartialData?: boolean; + skipPollAttempt?: () => boolean; variables?: TVariables; } @@ -2899,7 +2900,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:395:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:31:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts diff --git a/.changeset/swift-zoos-collect.md b/.changeset/swift-zoos-collect.md new file mode 100644 index 00000000000..ee80ede295e --- /dev/null +++ b/.changeset/swift-zoos-collect.md @@ -0,0 +1,19 @@ +--- +@apollo/client: minor +--- + +Adds a new `skipPollAttempt` callback function that's called whenever a refetch attempt occurs while polling. If the function returns `true`, the refetch is skipped and not reattempted until the next poll interval. This will solve the frequent use-case of disabling polling when the window is inactive. + +```ts +useQuery(QUERY, { + pollInterval: 1000, + skipPollAttempt: () => document.hidden // or !document.hasFocus() +}); +// or define it globally +new ApolloClient({ + defaultOptions: { + watchQuery: { + skipPollAttempt: () => document.hidden // or !document.hasFocus() + } + } +}) diff --git a/.size-limits.json b/.size-limits.json index f9e9c6f50a8..e23614a183f 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38546, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32318 + "dist/apollo-client.min.cjs": 38576, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32352 } diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 8516678dfc8..4822807e565 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -776,7 +776,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, const maybeFetch = () => { if (this.pollingInfo) { - if (!isNetworkRequestInFlight(this.queryInfo.networkStatus)) { + if ( + !isNetworkRequestInFlight(this.queryInfo.networkStatus) && + !this.options.skipPollAttempt?.() + ) { this.reobserve( { // Most fetchPolicy options don't make sense to use in a polling context, as diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index b74659b4a99..7c49d861097 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -176,6 +176,13 @@ export interface WatchQueryOptions< /** {@inheritDoc @apollo/client!QueryOptions#canonizeResults:member} */ canonizeResults?: boolean; + + /** + * A callback function that's called whenever a refetch attempt occurs + * while polling. If the function returns `true`, the refetch is + * skipped and not reattempted until the next poll interval. + */ + skipPollAttempt?: () => boolean; } export interface NextFetchPolicyContext< diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 0c6a56edcdc..2e6f1e3a125 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -2092,6 +2092,200 @@ describe("useQuery Hook", () => { unmount(); result.current.stopPolling(); }); + + describe("should prevent fetches when `skipPollAttempt` returns `false`", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + it("when defined as a global default option", async () => { + const skipPollAttempt = jest.fn().mockImplementation(() => false); + + const query = gql` + { + hello + } + `; + const link = mockSingleLink( + { + request: { query }, + result: { data: { hello: "world 1" } }, + }, + { + request: { query }, + result: { data: { hello: "world 2" } }, + }, + { + request: { query }, + result: { data: { hello: "world 3" } }, + } + ); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + defaultOptions: { + watchQuery: { + skipPollAttempt, + }, + }, + }); + + const wrapper = ({ children }: any) => ( + {children} + ); + + const { result } = renderHook( + () => useQuery(query, { pollInterval: 10 }), + { wrapper } + ); + + expect(result.current.loading).toBe(true); + expect(result.current.data).toBe(undefined); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 1" }); + }, + { interval: 1 } + ); + + expect(result.current.loading).toBe(false); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 2" }); + }, + { interval: 1 } + ); + + skipPollAttempt.mockImplementation(() => true); + expect(result.current.loading).toBe(false); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + skipPollAttempt.mockImplementation(() => false); + expect(result.current.loading).toBe(false); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 3" }); + }, + { interval: 1 } + ); + }); + + it("when defined for a single query", async () => { + const skipPollAttempt = jest.fn().mockImplementation(() => false); + + const query = gql` + { + hello + } + `; + const mocks = [ + { + request: { query }, + result: { data: { hello: "world 1" } }, + }, + { + request: { query }, + result: { data: { hello: "world 2" } }, + }, + { + request: { query }, + result: { data: { hello: "world 3" } }, + }, + ]; + + const cache = new InMemoryCache(); + const wrapper = ({ children }: any) => ( + + {children} + + ); + + const { result } = renderHook( + () => + useQuery(query, { + pollInterval: 10, + skipPollAttempt, + }), + { wrapper } + ); + + expect(result.current.loading).toBe(true); + expect(result.current.data).toBe(undefined); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 1" }); + }, + { interval: 1 } + ); + + expect(result.current.loading).toBe(false); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 2" }); + }, + { interval: 1 } + ); + + skipPollAttempt.mockImplementation(() => true); + expect(result.current.loading).toBe(false); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + skipPollAttempt.mockImplementation(() => false); + expect(result.current.loading).toBe(false); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 3" }); + }, + { interval: 1 } + ); + }); + }); }); describe("Error handling", () => { From d05297d9873ffb3ef52445f9f44fe1bc0499a4d7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 13:56:22 -0700 Subject: [PATCH 47/90] Fix changeset formatting --- .changeset/swift-zoos-collect.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/swift-zoos-collect.md b/.changeset/swift-zoos-collect.md index ee80ede295e..b3e988b8f0a 100644 --- a/.changeset/swift-zoos-collect.md +++ b/.changeset/swift-zoos-collect.md @@ -1,5 +1,5 @@ --- -@apollo/client: minor +"@apollo/client": minor --- Adds a new `skipPollAttempt` callback function that's called whenever a refetch attempt occurs while polling. If the function returns `true`, the refetch is skipped and not reattempted until the next poll interval. This will solve the frequent use-case of disabling polling when the window is inactive. From 221dd99ffd1990f8bd0392543af35e9b08d0fed8 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 8 Dec 2023 13:48:42 +0100 Subject: [PATCH 48/90] Allow usage of WeakMap in React Native Hermes (#10804) Co-authored-by: phryneas --- .changeset/unlucky-rats-decide.md | 5 +++++ .size-limits.json | 4 ++-- src/utilities/common/canUse.ts | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 .changeset/unlucky-rats-decide.md diff --git a/.changeset/unlucky-rats-decide.md b/.changeset/unlucky-rats-decide.md new file mode 100644 index 00000000000..9be1d2d3961 --- /dev/null +++ b/.changeset/unlucky-rats-decide.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +use WeakMap in React Native with Hermes diff --git a/.size-limits.json b/.size-limits.json index e23614a183f..fb873241928 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38576, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32352 + "dist/apollo-client.min.cjs": 38589, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32365 } diff --git a/src/utilities/common/canUse.ts b/src/utilities/common/canUse.ts index 72e56c70388..217cc01158b 100644 --- a/src/utilities/common/canUse.ts +++ b/src/utilities/common/canUse.ts @@ -2,7 +2,9 @@ import { maybe } from "../globals/index.js"; export const canUseWeakMap = typeof WeakMap === "function" && - maybe(() => navigator.product) !== "ReactNative"; + !maybe( + () => navigator.product == "ReactNative" && !(global as any).HermesInternal + ); export const canUseWeakSet = typeof WeakSet === "function"; From 2a471646616e3af1b5c039e961f8d5717fad8f32 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 14 Dec 2023 13:08:11 +0100 Subject: [PATCH 49/90] Persisted Query Link: improve memory management (#11369) --- .../api-report-link_persisted-queries.md | 4 +- .changeset/thick-tips-cry.md | 9 + .size-limits.json | 4 +- .../__tests__/persisted-queries.test.ts | 63 +++- .../__tests__/react.test.tsx | 8 +- src/link/persisted-queries/index.ts | 283 +++++++++--------- 6 files changed, 232 insertions(+), 139 deletions(-) create mode 100644 .changeset/thick-tips-cry.md diff --git a/.api-reports/api-report-link_persisted-queries.md b/.api-reports/api-report-link_persisted-queries.md index dda0d3dd1db..353d48364e6 100644 --- a/.api-reports/api-report-link_persisted-queries.md +++ b/.api-reports/api-report-link_persisted-queries.md @@ -57,7 +57,9 @@ interface BaseOptions { // Warning: (ae-forgotten-export) The symbol "ApolloLink" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const createPersistedQueryLink: (options: PersistedQueryLink.Options) => ApolloLink; +export const createPersistedQueryLink: (options: PersistedQueryLink.Options) => ApolloLink & { + resetHashCache: () => void; +}; // @public (undocumented) interface DefaultContext extends Record { diff --git a/.changeset/thick-tips-cry.md b/.changeset/thick-tips-cry.md new file mode 100644 index 00000000000..407513ec1c7 --- /dev/null +++ b/.changeset/thick-tips-cry.md @@ -0,0 +1,9 @@ +--- +"@apollo/client": patch +--- + +Persisted Query Link: improve memory management +* use LRU `WeakCache` instead of `WeakMap` to keep a limited number of hash results +* hash cache is initiated lazily, only when needed +* expose `persistedLink.resetHashCache()` method +* reset hash cache if the upstream server reports it doesn't accept persisted queries diff --git a/.size-limits.json b/.size-limits.json index fb873241928..1ac8be3319e 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38589, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32365 + "dist/apollo-client.min.cjs": 38535, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32310 } diff --git a/src/link/persisted-queries/__tests__/persisted-queries.test.ts b/src/link/persisted-queries/__tests__/persisted-queries.test.ts index ea8b56e660a..32d75fe5136 100644 --- a/src/link/persisted-queries/__tests__/persisted-queries.test.ts +++ b/src/link/persisted-queries/__tests__/persisted-queries.test.ts @@ -66,7 +66,7 @@ const giveUpResponse = JSON.stringify({ errors: giveUpErrors }); const giveUpResponseWithCode = JSON.stringify({ errors: giveUpErrorsWithCode }); const multiResponse = JSON.stringify({ errors: multipleErrors }); -export function sha256(data: string) { +function sha256(data: string) { const hash = crypto.createHash("sha256"); hash.update(data); return hash.digest("hex"); @@ -151,6 +151,32 @@ describe("happy path", () => { }, reject); }); + it("clears the cache when calling `resetHashCache`", async () => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); + + const hashRefs: WeakRef[] = []; + function hash(query: string) { + const newHash = new String(query); + hashRefs.push(new WeakRef(newHash)); + return newHash as string; + } + const persistedLink = createPersistedQuery({ sha256: hash }); + await new Promise((complete) => + execute(persistedLink.concat(createHttpLink()), { + query, + variables, + }).subscribe({ complete }) + ); + + await expect(hashRefs[0]).not.toBeGarbageCollected(); + persistedLink.resetHashCache(); + await expect(hashRefs[0]).toBeGarbageCollected(); + }); + itAsync("supports loading the hash from other method", (resolve, reject) => { fetchMock.post( "/graphql", @@ -517,6 +543,41 @@ describe("failure path", () => { }) ); + it.each([ + ["error message", giveUpResponse], + ["error code", giveUpResponseWithCode], + ] as const)( + "clears the cache when receiving NotSupported error (%s)", + async (_description, failingResponse) => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: failingResponse })), + { repeat: 1 } + ); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); + + const hashRefs: WeakRef[] = []; + function hash(query: string) { + const newHash = new String(query); + hashRefs.push(new WeakRef(newHash)); + return newHash as string; + } + const persistedLink = createPersistedQuery({ sha256: hash }); + await new Promise((complete) => + execute(persistedLink.concat(createHttpLink()), { + query, + variables, + }).subscribe({ complete }) + ); + + await expect(hashRefs[0]).toBeGarbageCollected(); + } + ); + itAsync("works with multiple errors", (resolve, reject) => { fetchMock.post( "/graphql", diff --git a/src/link/persisted-queries/__tests__/react.test.tsx b/src/link/persisted-queries/__tests__/react.test.tsx index 07c3fe7375e..b05e7d98f32 100644 --- a/src/link/persisted-queries/__tests__/react.test.tsx +++ b/src/link/persisted-queries/__tests__/react.test.tsx @@ -4,6 +4,7 @@ import * as ReactDOM from "react-dom/server"; import gql from "graphql-tag"; import { print } from "graphql"; import fetchMock from "fetch-mock"; +import crypto from "crypto"; import { ApolloProvider } from "../../../react/context"; import { InMemoryCache as Cache } from "../../../cache/inmemory/inMemoryCache"; @@ -12,7 +13,12 @@ import { createHttpLink } from "../../http/createHttpLink"; import { graphql } from "../../../react/hoc/graphql"; import { getDataFromTree } from "../../../react/ssr/getDataFromTree"; import { createPersistedQueryLink as createPersistedQuery, VERSION } from ".."; -import { sha256 } from "./persisted-queries.test"; + +function sha256(data: string) { + const hash = crypto.createHash("sha256"); + hash.update(data); + return hash.digest("hex"); +} // Necessary configuration in order to mock multiple requests // to a single (/graphql) endpoint diff --git a/src/link/persisted-queries/index.ts b/src/link/persisted-queries/index.ts index fa4c64bac58..95c4129c802 100644 --- a/src/link/persisted-queries/index.ts +++ b/src/link/persisted-queries/index.ts @@ -12,6 +12,7 @@ import type { import { Observable, compact, isNonEmptyArray } from "../../utilities/index.js"; import type { NetworkError } from "../../errors/index.js"; import type { ServerError } from "../utils/index.js"; +import { WeakCache } from "@wry/caches"; export const VERSION = 1; @@ -93,7 +94,10 @@ function operationDefinesMutation(operation: Operation) { export const createPersistedQueryLink = ( options: PersistedQueryLink.Options ) => { - const hashesByQuery = new WeakMap>(); + let hashesByQuery: WeakCache> | undefined; + function resetHashCache() { + hashesByQuery = undefined; + } // Ensure a SHA-256 hash function is provided, if a custom hash // generation function is not provided. We don't supply a SHA-256 hash // function by default, to avoid forcing one as a dependency. Developers @@ -135,149 +139,160 @@ export const createPersistedQueryLink = ( // what to do with the bogus query. return getHashPromise(query); } + if (!hashesByQuery) { + hashesByQuery = + new WeakCache(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); + } let hash = hashesByQuery.get(query)!; if (!hash) hashesByQuery.set(query, (hash = getHashPromise(query))); return hash; } - return new ApolloLink((operation, forward) => { - invariant( - forward, - "PersistedQueryLink cannot be the last link in the chain." - ); - - const { query } = operation; - - return new Observable((observer: Observer) => { - let subscription: ObservableSubscription; - let retried = false; - let originalFetchOptions: any; - let setFetchOptions = false; - const maybeRetry = ( - { - response, - networkError, - }: { response?: ExecutionResult; networkError?: ServerError }, - cb: () => void - ) => { - if (!retried && ((response && response.errors) || networkError)) { - retried = true; - - const graphQLErrors: GraphQLError[] = []; - - const responseErrors = response && response.errors; - if (isNonEmptyArray(responseErrors)) { - graphQLErrors.push(...responseErrors); - } - - // Network errors can return GraphQL errors on for example a 403 - let networkErrors; - if (typeof networkError?.result !== "string") { - networkErrors = - networkError && - networkError.result && - (networkError.result.errors as GraphQLError[]); - } - if (isNonEmptyArray(networkErrors)) { - graphQLErrors.push(...networkErrors); - } - - const disablePayload: ErrorResponse = { + return Object.assign( + new ApolloLink((operation, forward) => { + invariant( + forward, + "PersistedQueryLink cannot be the last link in the chain." + ); + + const { query } = operation; + + return new Observable((observer: Observer) => { + let subscription: ObservableSubscription; + let retried = false; + let originalFetchOptions: any; + let setFetchOptions = false; + const maybeRetry = ( + { response, networkError, - operation, - graphQLErrors: - isNonEmptyArray(graphQLErrors) ? graphQLErrors : void 0, - meta: processErrors(graphQLErrors), - }; - - // if the server doesn't support persisted queries, don't try anymore - supportsPersistedQueries = !disable(disablePayload); - - // if its not found, we can try it again, otherwise just report the error - if (retry(disablePayload)) { - // need to recall the link chain - if (subscription) subscription.unsubscribe(); - // actually send the query this time - operation.setContext({ - http: { - includeQuery: true, - includeExtensions: supportsPersistedQueries, - }, - fetchOptions: { - // Since we're including the full query, which may be - // large, we should send it in the body of a POST request. - // See issue #7456. - method: "POST", - }, - }); - if (setFetchOptions) { - operation.setContext({ fetchOptions: originalFetchOptions }); + }: { response?: ExecutionResult; networkError?: ServerError }, + cb: () => void + ) => { + if (!retried && ((response && response.errors) || networkError)) { + retried = true; + + const graphQLErrors: GraphQLError[] = []; + + const responseErrors = response && response.errors; + if (isNonEmptyArray(responseErrors)) { + graphQLErrors.push(...responseErrors); } - subscription = forward(operation).subscribe(handler); - return; - } - } - cb(); - }; - const handler = { - next: (response: ExecutionResult) => { - maybeRetry({ response }, () => observer.next!(response)); - }, - error: (networkError: ServerError) => { - maybeRetry({ networkError }, () => observer.error!(networkError)); - }, - complete: observer.complete!.bind(observer), - }; - - // don't send the query the first time - operation.setContext({ - http: { - includeQuery: !supportsPersistedQueries, - includeExtensions: supportsPersistedQueries, - }, - }); + // Network errors can return GraphQL errors on for example a 403 + let networkErrors; + if (typeof networkError?.result !== "string") { + networkErrors = + networkError && + networkError.result && + (networkError.result.errors as GraphQLError[]); + } + if (isNonEmptyArray(networkErrors)) { + graphQLErrors.push(...networkErrors); + } - // If requested, set method to GET if there are no mutations. Remember the - // original fetchOptions so we can restore them if we fall back to a - // non-hashed request. - if ( - useGETForHashedQueries && - supportsPersistedQueries && - !operationDefinesMutation(operation) - ) { - operation.setContext( - ({ fetchOptions = {} }: { fetchOptions: Record }) => { - originalFetchOptions = fetchOptions; - return { - fetchOptions: { - ...fetchOptions, - method: "GET", - }, + const disablePayload: ErrorResponse = { + response, + networkError, + operation, + graphQLErrors: + isNonEmptyArray(graphQLErrors) ? graphQLErrors : void 0, + meta: processErrors(graphQLErrors), }; + + // if the server doesn't support persisted queries, don't try anymore + supportsPersistedQueries = !disable(disablePayload); + if (!supportsPersistedQueries) { + // clear hashes from cache, we don't need them anymore + resetHashCache(); + } + + // if its not found, we can try it again, otherwise just report the error + if (retry(disablePayload)) { + // need to recall the link chain + if (subscription) subscription.unsubscribe(); + // actually send the query this time + operation.setContext({ + http: { + includeQuery: true, + includeExtensions: supportsPersistedQueries, + }, + fetchOptions: { + // Since we're including the full query, which may be + // large, we should send it in the body of a POST request. + // See issue #7456. + method: "POST", + }, + }); + if (setFetchOptions) { + operation.setContext({ fetchOptions: originalFetchOptions }); + } + subscription = forward(operation).subscribe(handler); + + return; + } } - ); - setFetchOptions = true; - } - - if (supportsPersistedQueries) { - getQueryHash(query) - .then((sha256Hash) => { - operation.extensions.persistedQuery = { - version: VERSION, - sha256Hash, - }; - subscription = forward(operation).subscribe(handler); - }) - .catch(observer.error!.bind(observer)); - } else { - subscription = forward(operation).subscribe(handler); - } - - return () => { - if (subscription) subscription.unsubscribe(); - }; - }); - }); + cb(); + }; + const handler = { + next: (response: ExecutionResult) => { + maybeRetry({ response }, () => observer.next!(response)); + }, + error: (networkError: ServerError) => { + maybeRetry({ networkError }, () => observer.error!(networkError)); + }, + complete: observer.complete!.bind(observer), + }; + + // don't send the query the first time + operation.setContext({ + http: { + includeQuery: !supportsPersistedQueries, + includeExtensions: supportsPersistedQueries, + }, + }); + + // If requested, set method to GET if there are no mutations. Remember the + // original fetchOptions so we can restore them if we fall back to a + // non-hashed request. + if ( + useGETForHashedQueries && + supportsPersistedQueries && + !operationDefinesMutation(operation) + ) { + operation.setContext( + ({ fetchOptions = {} }: { fetchOptions: Record }) => { + originalFetchOptions = fetchOptions; + return { + fetchOptions: { + ...fetchOptions, + method: "GET", + }, + }; + } + ); + setFetchOptions = true; + } + + if (supportsPersistedQueries) { + getQueryHash(query) + .then((sha256Hash) => { + operation.extensions.persistedQuery = { + version: VERSION, + sha256Hash, + }; + subscription = forward(operation).subscribe(handler); + }) + .catch(observer.error!.bind(observer)); + } else { + subscription = forward(operation).subscribe(handler); + } + + return () => { + if (subscription) subscription.unsubscribe(); + }; + }); + }), + { resetHashCache } + ); }; From f5420b0edf8bcb7ce08e988ea96e2940ac74150a Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 15 Dec 2023 10:51:58 +0100 Subject: [PATCH 50/90] add central configuration for Apollo Client cache sizes (#11408) * `print`: use `WeakCache` instead of `WeakMap` * format * pull in memory testing tools from PR 11358 * Persisted Query Link: improve memory management * re-add accidentally removed dependency * update api * update size limit * size-limit * fix test failure * better cleanup of interval/timeout * apply formatting * remove unneccessary type * format again after updating prettier * add central confiuguration for Apollo Client cache sizes * resolve import cycle * add exports * reduce cache collection throttle timeout * typo in comment * fix circular import * size-limits * update type to remove `WeakKey` * api-extractor * work around ES5 class compat * update api-report * fix typo in comment * add more caches * add more caches * chores * add type export * update test * chores * formatting * adjust more tests * rename to `AutoCleaned*Cache`, mark @internal * Update src/utilities/caching/sizes.ts Co-authored-by: Jerel Miller * chores * size-limits * unify comment release tags * update exports * naming & lazy bundling approach through inlining * chores * size --------- Co-authored-by: Jerel Miller --- .api-reports/api-report-cache.md | 2 +- .api-reports/api-report-core.md | 8 +- .api-reports/api-report-react.md | 6 +- .api-reports/api-report-react_components.md | 6 +- .api-reports/api-report-react_context.md | 6 +- .api-reports/api-report-react_hoc.md | 6 +- .api-reports/api-report-react_hooks.md | 6 +- .api-reports/api-report-react_ssr.md | 6 +- .api-reports/api-report-testing.md | 6 +- .api-reports/api-report-testing_core.md | 6 +- .api-reports/api-report-utilities.md | 75 ++++- .api-reports/api-report.md | 8 +- .size-limits.json | 4 +- api-extractor.json | 5 + src/__tests__/__snapshots__/exports.ts.snap | 3 + src/cache/core/cache.ts | 14 +- src/cache/inmemory/__tests__/cache.ts | 17 +- src/cache/inmemory/__tests__/readFromStore.ts | 6 +- src/cache/inmemory/fragmentRegistry.ts | 29 +- src/cache/inmemory/inMemoryCache.ts | 7 +- src/cache/inmemory/readFromStore.ts | 12 +- src/cache/inmemory/types.ts | 5 + src/core/QueryManager.ts | 10 +- src/link/persisted-queries/index.ts | 16 +- .../removeTypenameFromVariables.ts | 34 ++- src/react/parser/index.ts | 13 +- src/utilities/caching/__tests__/sizes.test.ts | 11 + src/utilities/caching/caches.ts | 80 ++++++ src/utilities/caching/index.ts | 3 + src/utilities/caching/sizes.ts | 264 ++++++++++++++++++ src/utilities/common/canonicalStringify.ts | 13 +- src/utilities/graphql/DocumentTransform.ts | 3 +- src/utilities/graphql/print.ts | 15 +- src/utilities/index.ts | 8 + 34 files changed, 628 insertions(+), 85 deletions(-) create mode 100644 src/utilities/caching/__tests__/sizes.test.ts create mode 100644 src/utilities/caching/caches.ts create mode 100644 src/utilities/caching/index.ts create mode 100644 src/utilities/caching/sizes.ts diff --git a/.api-reports/api-report-cache.md b/.api-reports/api-report-cache.md index 50588f8f4c0..1f2efd837fc 100644 --- a/.api-reports/api-report-cache.md +++ b/.api-reports/api-report-cache.md @@ -551,7 +551,7 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { fragments?: FragmentRegistryAPI; // (undocumented) possibleTypes?: PossibleTypesMap; - // (undocumented) + // @deprecated (undocumented) resultCacheMaxSize?: number; // (undocumented) resultCaching?: boolean; diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index e16b21db97b..d2ec18f333b 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -1013,7 +1013,7 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { fragments?: FragmentRegistryAPI; // (undocumented) possibleTypes?: PossibleTypesMap; - // (undocumented) + // @deprecated (undocumented) resultCacheMaxSize?: number; // (undocumented) resultCaching?: boolean; @@ -2113,9 +2113,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:395:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:121:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:396:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 7acf9ba2c5a..7a8606ad8fe 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -2250,9 +2250,9 @@ interface WatchQueryOptions(it: (...args: TArgs // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:395:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:121:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:396:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index ad2f42b7825..ca6f6c60b4e 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -1580,9 +1580,9 @@ export function withWarningSpy(it: (...args: TArgs // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:395:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:121:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:396:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 9ccfda99640..65911cc6b02 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -22,12 +22,14 @@ import type { Observer } from 'zen-observable-ts'; import type { OperationDefinitionNode } from 'graphql'; import type { SelectionNode } from 'graphql'; import type { SelectionSetNode } from 'graphql'; +import { StrongCache } from '@wry/caches'; import type { Subscriber } from 'zen-observable-ts'; import { Trie } from '@wry/trie'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; import type { ValueNode } from 'graphql'; import type { VariableDefinitionNode } from 'graphql'; import type { VariableNode } from 'graphql'; +import { WeakCache } from '@wry/caches'; // @public (undocumented) export const addTypenameToDocument: ((doc: TNode) => TNode) & { @@ -324,6 +326,18 @@ export type AsStoreObject(observable: Observable, mapFn: (value: V) => R | PromiseLike, catchFn?: (error: any) => R | PromiseLike): Observable; +// @internal +export const AutoCleanedStrongCache: typeof StrongCache; + +// @internal (undocumented) +export type AutoCleanedStrongCache = StrongCache; + +// @internal +export const AutoCleanedWeakCache: typeof WeakCache; + +// @internal (undocumented) +export type AutoCleanedWeakCache = WeakCache; + // Warning: (ae-forgotten-export) The symbol "InMemoryCache" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -444,6 +458,27 @@ class CacheGroup { resetCaching(): void; } +// @public +export interface CacheSizes { + "cache.fragmentQueryDocuments": number; + "documentTransform.cache": number; + "fragmentRegistry.findFragmentSpreads": number; + "fragmentRegistry.lookup": number; + "fragmentRegistry.transform": number; + "inMemoryCache.executeSelectionSet": number; + "inMemoryCache.executeSubSelectedArray": number; + "inMemoryCache.maybeBroadcastWatch": number; + "PersistedQueryLink.persistedQueryHashes": number; + "queryManager.getDocumentInfo": number; + "removeTypenameFromVariables.getVariableDefinitions": number; + canonicalStringify: number; + parser: number; + print: number; +} + +// @public +export const cacheSizes: Partial; + // @public (undocumented) const enum CacheWriteBehavior { // (undocumented) @@ -667,6 +702,38 @@ type DeepPartialReadonlySet = {} & ReadonlySet>; // @public (undocumented) type DeepPartialSet = {} & Set>; +// @public (undocumented) +export const enum defaultCacheSizes { + // (undocumented) + "cache.fragmentQueryDocuments" = 1000, + // (undocumented) + "documentTransform.cache" = 2000, + // (undocumented) + "fragmentRegistry.findFragmentSpreads" = 4000, + // (undocumented) + "fragmentRegistry.lookup" = 1000, + // (undocumented) + "fragmentRegistry.transform" = 2000, + // (undocumented) + "inMemoryCache.executeSelectionSet" = 10000, + // (undocumented) + "inMemoryCache.executeSubSelectedArray" = 5000, + // (undocumented) + "inMemoryCache.maybeBroadcastWatch" = 5000, + // (undocumented) + "PersistedQueryLink.persistedQueryHashes" = 2000, + // (undocumented) + "queryManager.getDocumentInfo" = 2000, + // (undocumented) + "removeTypenameFromVariables.getVariableDefinitions" = 2000, + // (undocumented) + canonicalStringify = 1000, + // (undocumented) + parser = 1000, + // (undocumented) + print = 2000 +} + // @public (undocumented) interface DefaultContext extends Record { } @@ -1198,7 +1265,7 @@ interface InMemoryCacheConfig extends ApolloReducerConfig { // // (undocumented) possibleTypes?: PossibleTypesMap; - // (undocumented) + // @deprecated (undocumented) resultCacheMaxSize?: number; // (undocumented) resultCaching?: boolean; @@ -2463,9 +2530,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:395:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:121:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:396:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 5a22a0eefdd..d3549849065 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -1199,7 +1199,7 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { fragments?: FragmentRegistryAPI; // (undocumented) possibleTypes?: PossibleTypesMap; - // (undocumented) + // @deprecated (undocumented) resultCacheMaxSize?: number; // (undocumented) resultCaching?: boolean; @@ -2897,9 +2897,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:395:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:121:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:396:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts diff --git a/.size-limits.json b/.size-limits.json index 1ac8be3319e..f6a846be06c 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38535, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32310 + "dist/apollo-client.min.cjs": 38831, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32603 } diff --git a/api-extractor.json b/api-extractor.json index 5a0a74befa3..b257d21f772 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -119,6 +119,11 @@ "logLevel": "none" }, + "ae-internal-missing-underscore": { + "logLevel": "none", + "addToApiReportFile": false + }, + "ae-unresolved-link": { "logLevel": "warning", "addToApiReportFile": true diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 70229c88a17..ba76b853b7c 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -383,6 +383,8 @@ Array [ exports[`exports of public entry points @apollo/client/utilities 1`] = ` Array [ + "AutoCleanedStrongCache", + "AutoCleanedWeakCache", "Concast", "DEV", "DeepMerger", @@ -392,6 +394,7 @@ Array [ "argumentsObjectFromField", "asyncMap", "buildQueryFromSelectionSet", + "cacheSizes", "canUseAsyncIteratorSymbol", "canUseDOM", "canUseLayoutEffect", diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index 44f283a2723..b90871cdd82 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -2,9 +2,14 @@ import type { DocumentNode } from "graphql"; import { wrap } from "optimism"; import type { StoreObject, Reference } from "../../utilities/index.js"; -import { getFragmentQueryDocument } from "../../utilities/index.js"; +import { + cacheSizes, + defaultCacheSizes, + getFragmentQueryDocument, +} from "../../utilities/index.js"; import type { DataProxy } from "./types/DataProxy.js"; import type { Cache } from "./types/Cache.js"; +import { WeakCache } from "@wry/caches"; export type Transaction = (c: ApolloCache) => void; @@ -137,7 +142,12 @@ export abstract class ApolloCache implements DataProxy { // Make sure we compute the same (===) fragment query document every // time we receive the same fragment in readFragment. - private getFragmentDoc = wrap(getFragmentQueryDocument); + private getFragmentDoc = wrap(getFragmentQueryDocument, { + max: + cacheSizes["cache.fragmentQueryDocuments"] || + defaultCacheSizes["cache.fragmentQueryDocuments"], + cache: WeakCache, + }); public readFragment( options: Cache.ReadFragmentOptions, diff --git a/src/cache/inmemory/__tests__/cache.ts b/src/cache/inmemory/__tests__/cache.ts index edaa63fb4d4..2d426ed7207 100644 --- a/src/cache/inmemory/__tests__/cache.ts +++ b/src/cache/inmemory/__tests__/cache.ts @@ -19,6 +19,7 @@ import { StoreWriter } from "../writeToStore"; import { ObjectCanon } from "../object-canon"; import { TypePolicies } from "../policies"; import { spyOnConsole } from "../../../testing/internal"; +import { defaultCacheSizes } from "../../../utilities"; disableFragmentWarnings(); @@ -2119,15 +2120,17 @@ describe("Cache", () => { }); describe("resultCacheMaxSize", () => { - const defaultMaxSize = Math.pow(2, 16); - it("uses default max size on caches if resultCacheMaxSize is not configured", () => { const cache = new InMemoryCache(); - expect(cache["maybeBroadcastWatch"].options.max).toBe(defaultMaxSize); + expect(cache["maybeBroadcastWatch"].options.max).toBe( + defaultCacheSizes["inMemoryCache.maybeBroadcastWatch"] + ); expect(cache["storeReader"]["executeSelectionSet"].options.max).toBe( - defaultMaxSize + defaultCacheSizes["inMemoryCache.executeSelectionSet"] + ); + expect(cache["getFragmentDoc"].options.max).toBe( + defaultCacheSizes["cache.fragmentQueryDocuments"] ); - expect(cache["getFragmentDoc"].options.max).toBe(defaultMaxSize); }); it("configures max size on caches when resultCacheMaxSize is set", () => { @@ -2137,7 +2140,9 @@ describe("resultCacheMaxSize", () => { expect(cache["storeReader"]["executeSelectionSet"].options.max).toBe( resultCacheMaxSize ); - expect(cache["getFragmentDoc"].options.max).toBe(defaultMaxSize); + expect(cache["getFragmentDoc"].options.max).toBe( + defaultCacheSizes["cache.fragmentQueryDocuments"] + ); }); }); diff --git a/src/cache/inmemory/__tests__/readFromStore.ts b/src/cache/inmemory/__tests__/readFromStore.ts index 972e252215c..2af1138cef8 100644 --- a/src/cache/inmemory/__tests__/readFromStore.ts +++ b/src/cache/inmemory/__tests__/readFromStore.ts @@ -17,14 +17,16 @@ import { isReference, TypedDocumentNode, } from "../../../core"; +import { defaultCacheSizes } from "../../../utilities"; describe("resultCacheMaxSize", () => { const cache = new InMemoryCache(); - const defaultMaxSize = Math.pow(2, 16); it("uses default max size on caches if resultCacheMaxSize is not configured", () => { const reader = new StoreReader({ cache }); - expect(reader["executeSelectionSet"].options.max).toBe(defaultMaxSize); + expect(reader["executeSelectionSet"].options.max).toBe( + defaultCacheSizes["inMemoryCache.executeSelectionSet"] + ); }); it("configures max size on caches when resultCacheMaxSize is set", () => { diff --git a/src/cache/inmemory/fragmentRegistry.ts b/src/cache/inmemory/fragmentRegistry.ts index 12cade01aea..28463594c9d 100644 --- a/src/cache/inmemory/fragmentRegistry.ts +++ b/src/cache/inmemory/fragmentRegistry.ts @@ -9,7 +9,12 @@ import { visit } from "graphql"; import { wrap } from "optimism"; import type { FragmentMap } from "../../utilities/index.js"; -import { getFragmentDefinitions } from "../../utilities/index.js"; +import { + cacheSizes, + defaultCacheSizes, + getFragmentDefinitions, +} from "../../utilities/index.js"; +import { WeakCache } from "@wry/caches"; export interface FragmentRegistryAPI { register(...fragments: DocumentNode[]): this; @@ -68,11 +73,29 @@ class FragmentRegistry implements FragmentRegistryAPI { const proto = FragmentRegistry.prototype; this.invalidate = (this.lookup = wrap(proto.lookup.bind(this), { makeCacheKey: (arg) => arg, + max: + cacheSizes["fragmentRegistry.lookup"] || + defaultCacheSizes["fragmentRegistry.lookup"], })).dirty; // This dirty function is bound to the wrapped lookup method. - this.transform = wrap(proto.transform.bind(this)); - this.findFragmentSpreads = wrap(proto.findFragmentSpreads.bind(this)); + this.transform = wrap(proto.transform.bind(this), { + cache: WeakCache, + max: + cacheSizes["fragmentRegistry.transform"] || + defaultCacheSizes["fragmentRegistry.transform"], + }); + this.findFragmentSpreads = wrap(proto.findFragmentSpreads.bind(this), { + cache: WeakCache, + max: + cacheSizes["fragmentRegistry.findFragmentSpreads"] || + defaultCacheSizes["fragmentRegistry.findFragmentSpreads"], + }); } + /* + * Note: + * This method is only memoized so it can serve as a dependency to `tranform`, + * so calling `invalidate` will invalidate cache entries for `transform`. + */ public lookup(fragmentName: string): FragmentDefinitionNode | null { return this.registry[fragmentName] || null; } diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index f00fed8767e..6e001cd9137 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -18,6 +18,8 @@ import { DocumentTransform, canonicalStringify, print, + cacheSizes, + defaultCacheSizes, } from "../../utilities/index.js"; import type { InMemoryCacheConfig, NormalizedCacheObject } from "./types.js"; import { StoreReader } from "./readFromStore.js"; @@ -124,7 +126,10 @@ export class InMemoryCache extends ApolloCache { return this.broadcastWatch(c, options); }, { - max: this.config.resultCacheMaxSize, + max: + this.config.resultCacheMaxSize || + cacheSizes["inMemoryCache.maybeBroadcastWatch"] || + defaultCacheSizes["inMemoryCache.maybeBroadcastWatch"], makeCacheKey: (c: Cache.WatchOptions) => { // Return a cache key (thus enabling result caching) only if we're // currently using a data store that can track cache dependencies. diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index a280948b87e..cc1ec9f0f6b 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -29,6 +29,8 @@ import { canUseWeakMap, compact, canonicalStringify, + cacheSizes, + defaultCacheSizes, } from "../../utilities/index.js"; import type { Cache } from "../core/types/Cache.js"; import type { @@ -193,7 +195,10 @@ export class StoreReader { return this.execSelectionSetImpl(options); }, { - max: this.config.resultCacheMaxSize, + max: + this.config.resultCacheMaxSize || + cacheSizes["inMemoryCache.executeSelectionSet"] || + defaultCacheSizes["inMemoryCache.executeSelectionSet"], keyArgs: execSelectionSetKeyArgs, // Note that the parameters of makeCacheKey are determined by the // array returned by keyArgs. @@ -219,7 +224,10 @@ export class StoreReader { return this.execSubSelectedArrayImpl(options); }, { - max: this.config.resultCacheMaxSize, + max: + this.config.resultCacheMaxSize || + cacheSizes["inMemoryCache.executeSubSelectedArray"] || + defaultCacheSizes["inMemoryCache.executeSubSelectedArray"], makeCacheKey({ field, array, context }) { if (supportsResultCaching(context.store)) { return context.store.makeCacheKey(field, array, context.varString); diff --git a/src/cache/inmemory/types.ts b/src/cache/inmemory/types.ts index 5a8d66ec300..207a802feb4 100644 --- a/src/cache/inmemory/types.ts +++ b/src/cache/inmemory/types.ts @@ -137,6 +137,11 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { resultCaching?: boolean; possibleTypes?: PossibleTypesMap; typePolicies?: TypePolicies; + /** + * @deprecated + * Please use `cacheSizes` instead. + * TODO: write docs page, add link here + */ resultCacheMaxSize?: number; canonizeResults?: boolean; fragments?: FragmentRegistryAPI; diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 84e01b89023..ba70751e3c8 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -4,11 +4,11 @@ import type { DocumentNode } from "graphql"; // TODO(brian): A hack until this issue is resolved (https://github.com/graphql/graphql-js/issues/3356) type OperationTypeNode = any; import { equal } from "@wry/equality"; -import { WeakCache } from "@wry/caches"; import type { ApolloLink, FetchResult } from "../link/core/index.js"; import { execute } from "../link/core/index.js"; import { + defaultCacheSizes, hasDirectives, isExecutionPatchIncrementalResult, isExecutionPatchResult, @@ -100,6 +100,7 @@ interface TransformCacheEntry { import type { DefaultOptions } from "./ApolloClient.js"; import { Trie } from "@wry/trie"; +import { AutoCleanedWeakCache, cacheSizes } from "../utilities/index.js"; export class QueryManager { public cache: ApolloCache; @@ -662,10 +663,13 @@ export class QueryManager { return this.documentTransform.transformDocument(document); } - private transformCache = new WeakCache< + private transformCache = new AutoCleanedWeakCache< DocumentNode, TransformCacheEntry - >(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); + >( + cacheSizes["queryManager.getDocumentInfo"] || + defaultCacheSizes["queryManager.getDocumentInfo"] + ); public getDocumentInfo(document: DocumentNode) { const { transformCache } = this; diff --git a/src/link/persisted-queries/index.ts b/src/link/persisted-queries/index.ts index 95c4129c802..400af11d6f5 100644 --- a/src/link/persisted-queries/index.ts +++ b/src/link/persisted-queries/index.ts @@ -12,7 +12,11 @@ import type { import { Observable, compact, isNonEmptyArray } from "../../utilities/index.js"; import type { NetworkError } from "../../errors/index.js"; import type { ServerError } from "../utils/index.js"; -import { WeakCache } from "@wry/caches"; +import { + cacheSizes, + AutoCleanedWeakCache, + defaultCacheSizes, +} from "../../utilities/index.js"; export const VERSION = 1; @@ -94,7 +98,9 @@ function operationDefinesMutation(operation: Operation) { export const createPersistedQueryLink = ( options: PersistedQueryLink.Options ) => { - let hashesByQuery: WeakCache> | undefined; + let hashesByQuery: + | AutoCleanedWeakCache> + | undefined; function resetHashCache() { hashesByQuery = undefined; } @@ -140,8 +146,10 @@ export const createPersistedQueryLink = ( return getHashPromise(query); } if (!hashesByQuery) { - hashesByQuery = - new WeakCache(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); + hashesByQuery = new AutoCleanedWeakCache( + cacheSizes["PersistedQueryLink.persistedQueryHashes"] || + defaultCacheSizes["PersistedQueryLink.persistedQueryHashes"] + ); } let hash = hashesByQuery.get(query)!; if (!hash) hashesByQuery.set(query, (hash = getHashPromise(query))); diff --git a/src/link/remove-typename/removeTypenameFromVariables.ts b/src/link/remove-typename/removeTypenameFromVariables.ts index b8c173b15d2..b713a5b4138 100644 --- a/src/link/remove-typename/removeTypenameFromVariables.ts +++ b/src/link/remove-typename/removeTypenameFromVariables.ts @@ -2,8 +2,14 @@ import { wrap } from "optimism"; import type { DocumentNode, TypeNode } from "graphql"; import { Kind, visit } from "graphql"; import { ApolloLink } from "../core/index.js"; -import { stripTypename, isPlainObject } from "../../utilities/index.js"; +import { + stripTypename, + isPlainObject, + cacheSizes, + defaultCacheSizes, +} from "../../utilities/index.js"; import type { OperationVariables } from "../../core/index.js"; +import { WeakCache } from "@wry/caches"; export const KEEP = "__KEEP"; @@ -95,17 +101,25 @@ function maybeStripTypename( return value; } -const getVariableDefinitions = wrap((document: DocumentNode) => { - const definitions: Record = {}; +const getVariableDefinitions = wrap( + (document: DocumentNode) => { + const definitions: Record = {}; - visit(document, { - VariableDefinition(node) { - definitions[node.variable.name.value] = unwrapType(node.type); - }, - }); + visit(document, { + VariableDefinition(node) { + definitions[node.variable.name.value] = unwrapType(node.type); + }, + }); - return definitions; -}); + return definitions; + }, + { + max: + cacheSizes["removeTypenameFromVariables.getVariableDefinitions"] || + defaultCacheSizes["removeTypenameFromVariables.getVariableDefinitions"], + cache: WeakCache, + } +); function unwrapType(node: TypeNode): string { switch (node.kind) { diff --git a/src/react/parser/index.ts b/src/react/parser/index.ts index 3c5dc3abc8d..5cca2c066f8 100644 --- a/src/react/parser/index.ts +++ b/src/react/parser/index.ts @@ -1,4 +1,3 @@ -import { WeakCache } from "@wry/caches"; import { invariant } from "../../utilities/globals/index.js"; import type { @@ -7,6 +6,11 @@ import type { VariableDefinitionNode, OperationDefinitionNode, } from "graphql"; +import { + AutoCleanedWeakCache, + cacheSizes, + defaultCacheSizes, +} from "../../utilities/index.js"; export enum DocumentType { Query, @@ -22,7 +26,7 @@ export interface IDocumentDefinition { let cache: | undefined - | WeakCache< + | AutoCleanedWeakCache< DocumentNode, { name: string; @@ -50,8 +54,9 @@ export function operationName(type: DocumentType) { // This parser is mostly used to safety check incoming documents. export function parser(document: DocumentNode): IDocumentDefinition { if (!cache) { - cache = - new WeakCache(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); + cache = new AutoCleanedWeakCache( + cacheSizes.parser || defaultCacheSizes.parser + ); } const cached = cache.get(document); if (cached) return cached; diff --git a/src/utilities/caching/__tests__/sizes.test.ts b/src/utilities/caching/__tests__/sizes.test.ts new file mode 100644 index 00000000000..8339c8950f3 --- /dev/null +++ b/src/utilities/caching/__tests__/sizes.test.ts @@ -0,0 +1,11 @@ +import { expectTypeOf } from "expect-type"; +import type { CacheSizes, defaultCacheSizes } from "../sizes"; + +test.skip("type tests", () => { + expectTypeOf().toMatchTypeOf< + keyof typeof defaultCacheSizes + >(); + expectTypeOf().toMatchTypeOf< + keyof CacheSizes + >(); +}); diff --git a/src/utilities/caching/caches.ts b/src/utilities/caching/caches.ts new file mode 100644 index 00000000000..6acbdb88d34 --- /dev/null +++ b/src/utilities/caching/caches.ts @@ -0,0 +1,80 @@ +import type { CommonCache } from "@wry/caches"; +import { WeakCache, StrongCache } from "@wry/caches"; + +const scheduledCleanup = new WeakSet>(); +function schedule(cache: CommonCache) { + if (!scheduledCleanup.has(cache)) { + scheduledCleanup.add(cache); + setTimeout(() => { + cache.clean(); + scheduledCleanup.delete(cache); + }, 100); + } +} +/** + * @internal + * A version of WeakCache that will auto-schedule a cleanup of the cache when + * a new item is added. + * Throttled to once per 100ms. + * + * @privateRemarks + * Should be used throughout the rest of the codebase instead of WeakCache, + * with the notable exception of usage in `wrap` from `optimism` - that one + * already handles cleanup and should remain a `WeakCache`. + */ +export const AutoCleanedWeakCache = function ( + max?: number | undefined, + dispose?: ((value: any, key: any) => void) | undefined +) { + /* + Some builds of `WeakCache` are function prototypes, some are classes. + This library still builds with an ES5 target, so we can't extend the + real classes. + Instead, we have to use this workaround until we switch to a newer build + target. + */ + const cache = new WeakCache(max, dispose); + cache.set = function (key: any, value: any) { + schedule(this); + return WeakCache.prototype.set.call(this, key, value); + }; + return cache; +} as any as typeof WeakCache; +/** + * @internal + */ +export type AutoCleanedWeakCache = WeakCache; + +/** + * @internal + * A version of StrongCache that will auto-schedule a cleanup of the cache when + * a new item is added. + * Throttled to once per 100ms. + * + * @privateRemarks + * Should be used throughout the rest of the codebase instead of StrongCache, + * with the notable exception of usage in `wrap` from `optimism` - that one + * already handles cleanup and should remain a `StrongCache`. + */ +export const AutoCleanedStrongCache = function ( + max?: number | undefined, + dispose?: ((value: any, key: any) => void) | undefined +) { + /* + Some builds of `StrongCache` are function prototypes, some are classes. + This library still builds with an ES5 target, so we can't extend the + real classes. + Instead, we have to use this workaround until we switch to a newer build + target. + */ + const cache = new StrongCache(max, dispose); + cache.set = function (key: any, value: any) { + schedule(this); + return StrongCache.prototype.set.call(this, key, value); + }; + return cache; +} as any as typeof StrongCache; +/** + * @internal + */ +export type AutoCleanedStrongCache = StrongCache; diff --git a/src/utilities/caching/index.ts b/src/utilities/caching/index.ts new file mode 100644 index 00000000000..159dc27fcfd --- /dev/null +++ b/src/utilities/caching/index.ts @@ -0,0 +1,3 @@ +export { AutoCleanedStrongCache, AutoCleanedWeakCache } from "./caches.js"; +export type { CacheSizes } from "./sizes.js"; +export { cacheSizes, defaultCacheSizes } from "./sizes.js"; diff --git a/src/utilities/caching/sizes.ts b/src/utilities/caching/sizes.ts new file mode 100644 index 00000000000..998537740a3 --- /dev/null +++ b/src/utilities/caching/sizes.ts @@ -0,0 +1,264 @@ +import { global } from "../globals/index.js"; + +declare global { + interface Window { + [cacheSizeSymbol]?: Partial; + } +} + +/** + * The cache sizes used by various Apollo Client caches. + * + * Note that these caches are all derivative and if an item is cache-collected, + * it's not the end of the world - the cached item will just be recalculated. + * + * As a result, these cache sizes should not be chosen to hold every value ever + * encountered, but rather to hold a reasonable number of values that can be + * assumed to be on the screen at any given time. + * + * We assume a "base value" of 1000 here, which is already very generous. + * In most applications, it will be very unlikely that 1000 different queries + * are on screen at the same time. + */ +export interface CacheSizes { + /** + * Cache size for the [`print`](../../utilities/graphql/print.ts) function. + * + * @defaultValue + * Defaults to `2000`. + * + * @remarks + * This method is called from the `QueryManager` and various `Link`s, + * always with the "serverQuery", so the server-facing part of a transformed + * DocumentNode. + */ + print: number; + /** + * Cache size for the [`parser`](../../react/parser/index.ts) function. + * + * @defaultValue + * Defaults to `1000`. + * + * @remarks + * This function is used directly in HOCs, and nowadays mainly accessed by + * calling `verifyDocumentType` from various hooks. + * It is called with a user-provided DocumentNode. + */ + parser: number; + /** + * Cache size for the `performWork` method of each [`DocumentTransform`](../../utilities/graphql/DocumentTransform.ts). + * + * @defaultValue + * Defaults to `2000`. + * + * @remarks + * This method is called from `transformDocument`, which is called from + * `QueryManager` with a user-provided DocumentNode. + * It is also called with already-transformed DocumentNodes, assuming the + * user provided additional transforms. + * + * The cache size here should be chosen with other DocumentTransforms in mind. + * For example, if there was a DocumentTransform that would take `n` DocumentNodes, + * and returned a differently-transformed DocumentNode depending if the app is + * online or offline, then we assume that the cache returns `2*n` documents. + * + * No user-provided DocumentNode will actually be "the last one", as we run the + * `defaultDocumentTransform` before *and* after the user-provided transforms. + * + * So if we assume that the user-provided transforms receive `n` documents and + * return `n` documents, the cache size should be `2*n`. + * + * If we assume that the user-provided transforms receive `n` documents and + * returns `2*n` documents, the cache size should be `3*n`. + * + * This size should also then be used in every other cache that mentions that + * it operates on a "transformed" DocumentNode. + */ + "documentTransform.cache": number; + /** + * Cache size for the `transformCache` used in the `getDocumentInfo` method of + * [`QueryManager`](../../core/QueryManager.ts). + * + * @defaultValue + * Defaults to `2000`. + * + * @remarks + * `getDocumentInfo` is called throughout the `QueryManager` with transformed + * DocumentNodes. + */ + "queryManager.getDocumentInfo": number; + /** + * Cache size for the `hashesByQuery` cache in the [`PersistedQueryLink`](../../link/persisted-queries/index.ts). + * + * @defaultValue + * Defaults to `2000`. + * + * @remarks + * This cache is used to cache the hashes of persisted queries. It is working with + * transformed DocumentNodes. + */ + "PersistedQueryLink.persistedQueryHashes": number; + /** + * Cache for the `sortingMap` used by [`canonicalStringify`](../../utilities/common/canonicalStringify.ts). + * + * @defaultValue + * Defaults to `1000`. + * + * @remarks + * This cache contains the sorted keys of objects that are stringified by + * `canonicalStringify`. + * It uses the stringified unsorted keys of objects as keys. + * The cache will not grow beyond the size of different object **shapes** + * encountered in an application, no matter how much actual data gets stringified. + */ + canonicalStringify: number; + /** + * Cache size for the `transform` method of [`FragmentRegistry`](../../cache/inmemory/fragmentRegistry.ts). + * + * @defaultValue + * Defaults to `2000`. + * + * @remarks + * This function is called as part of the `defaultDocumentTransform` which will be called with + * user-provided and already-transformed DocumentNodes. + * + */ + "fragmentRegistry.transform": number; + /** + * Cache size for the `lookup` method of [`FragmentRegistry`](../../cache/inmemory/fragmentRegistry.ts). + * + * @defaultValue + * Defaults to `1000`. + * + * @remarks + * This function is called with fragment names in the form of a string. + * + * Note: + * This function is a dependency of `transform`, so having a too small cache size here + * might involuntarily invalidate values in the `transform` cache. + */ + "fragmentRegistry.lookup": number; + /** + * Cache size for the `findFragmentSpreads` method of [`FragmentRegistry`](../../cache/inmemory/fragmentRegistry.ts). + * + * @defaultValue + * Defaults to `4000`. + * + * @remarks + * This function is called with transformed DocumentNodes, as well as recursively + * with every fragment spread referenced within that, or a fragment referenced by a + * fragment spread. + * + * Note: + * This function is a dependency of `transform`, so having a too small cache size here + * might involuntarily invalidate values in the `transform` cache. + */ + "fragmentRegistry.findFragmentSpreads": number; + /** + * Cache size for the `getFragmentDoc` method of [`ApolloCache`](../../cache/core/cache.ts). + * + * @defaultValue + * Defaults to `1000`. + * + * @remarks + * This function is called from `readFragment` with user-provided fragment definitions. + */ + "cache.fragmentQueryDocuments": number; + /** + * Cache size for the `getVariableDefinitions` function in [`removeTypenameFromVariables`](../../link/remove-typename/removeTypenameFromVariables.ts). + * + * @defaultValue + * Defaults to `2000`. + * + * @remarks + * This function is called in a link with transformed DocumentNodes. + */ + "removeTypenameFromVariables.getVariableDefinitions": number; + /** + * Cache size for the `maybeBroadcastWatch` method on [`InMemoryCache`](../../cache/inmemory/inMemoryCache.ts). + * + * `maybeBroadcastWatch` will be set to the `resultCacheMaxSize` option and + * will fall back to this configuration value if the option is not set. + * + * @defaultValue + * Defaults to `5000`. + * + * @remarks + * This method is used for dependency tracking in the `InMemoryCache` and + * prevents from unnecessary re-renders. + * It is recommended to keep this value significantly higher than the number of + * possible subscribers you will have active at the same time in your application + * at any time. + */ + "inMemoryCache.maybeBroadcastWatch": number; + /** + * Cache size for the `executeSelectionSet` method on [`StoreReader`](../../cache/inmemory/readFromStore.ts). + * + * `executeSelectionSet` will be set to the `resultCacheMaxSize` option and + * will fall back to this configuration value if the option is not set. + * + * @defaultValue + * Defaults to `10000`. + * + * @remarks + * Every object that is read from the cache will be cached here, so it is + * recommended to set this to a high value. + */ + "inMemoryCache.executeSelectionSet": number; + /** + * Cache size for the `executeSubSelectedArray` method on [`StoreReader`](../../cache/inmemory/readFromStore.ts). + * + * `executeSubSelectedArray` will be set to the `resultCacheMaxSize` option and + * will fall back to this configuration value if the option is not set. + * + * @defaultValue + * Defaults to `5000`. + * + * @remarks + * Every array that is read from the cache will be cached here, so it is + * recommended to set this to a high value. + */ + "inMemoryCache.executeSubSelectedArray": number; +} + +const cacheSizeSymbol = Symbol.for("apollo.cacheSize"); +/** + * + * The global cache size configuration for Apollo Client. + * + * @remarks + * + * You can directly modify this object, but any modification will + * only have an effect on caches that are created after the modification. + * + * So for global caches, such as `parser`, `canonicalStringify` and `print`, + * you might need to call `.reset` on them, which will essentially re-create them. + * + * Alternatively, you can set `globalThis[Symbol.for("apollo.cacheSize")]` before + * you load the Apollo Client package: + * + * @example + * ```ts + * globalThis[Symbol.for("apollo.cacheSize")] = { + * parser: 100 + * } satisfies Partial // the `satisfies` is optional if using TypeScript + * ``` + */ +export const cacheSizes: Partial = { ...global[cacheSizeSymbol] }; + +export const enum defaultCacheSizes { + parser = 1000, + canonicalStringify = 1000, + print = 2000, + "documentTransform.cache" = 2000, + "queryManager.getDocumentInfo" = 2000, + "PersistedQueryLink.persistedQueryHashes" = 2000, + "fragmentRegistry.transform" = 2000, + "fragmentRegistry.lookup" = 1000, + "fragmentRegistry.findFragmentSpreads" = 4000, + "cache.fragmentQueryDocuments" = 1000, + "removeTypenameFromVariables.getVariableDefinitions" = 2000, + "inMemoryCache.maybeBroadcastWatch" = 5000, + "inMemoryCache.executeSelectionSet" = 10000, + "inMemoryCache.executeSubSelectedArray" = 5000, +} diff --git a/src/utilities/common/canonicalStringify.ts b/src/utilities/common/canonicalStringify.ts index 021f23430e2..7c037b0a680 100644 --- a/src/utilities/common/canonicalStringify.ts +++ b/src/utilities/common/canonicalStringify.ts @@ -1,3 +1,9 @@ +import { + AutoCleanedStrongCache, + cacheSizes, + defaultCacheSizes, +} from "../../utilities/caching/index.js"; + /** * Like JSON.stringify, but with object keys always sorted in the same order. * @@ -24,14 +30,17 @@ export const canonicalStringify = Object.assign( // Clearing the sortingMap will reclaim all cached memory, without // affecting the logical results of canonicalStringify, but potentially // sacrificing performance until the cache is refilled. - sortingMap.clear(); + sortingMap = new AutoCleanedStrongCache( + cacheSizes.canonicalStringify || defaultCacheSizes.canonicalStringify + ); }, } ); // Values are JSON-serialized arrays of object keys (in any order), and values // are sorted arrays of the same keys. -const sortingMap = new Map(); +let sortingMap!: AutoCleanedStrongCache; +canonicalStringify.reset(); // The JSON.stringify function takes an optional second argument called a // replacer function. This function is called for each key-value pair in the diff --git a/src/utilities/graphql/DocumentTransform.ts b/src/utilities/graphql/DocumentTransform.ts index 7a5ce40fe4c..bf016aee0da 100644 --- a/src/utilities/graphql/DocumentTransform.ts +++ b/src/utilities/graphql/DocumentTransform.ts @@ -5,6 +5,7 @@ import { invariant } from "../globals/index.js"; import type { DocumentNode } from "graphql"; import { WeakCache } from "@wry/caches"; import { wrap } from "optimism"; +import { cacheSizes } from "../caching/index.js"; export type DocumentTransformCacheKey = ReadonlyArray; @@ -96,7 +97,7 @@ export class DocumentTransform { return stableCacheKeys.lookupArray(cacheKeys); } }, - max: 1000 /** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */, + max: cacheSizes["documentTransform.cache"], cache: WeakCache, } ); diff --git a/src/utilities/graphql/print.ts b/src/utilities/graphql/print.ts index 3ba1134c968..e32a3f048df 100644 --- a/src/utilities/graphql/print.ts +++ b/src/utilities/graphql/print.ts @@ -1,8 +1,12 @@ import type { ASTNode } from "graphql"; import { print as origPrint } from "graphql"; -import { WeakCache } from "@wry/caches"; +import { + AutoCleanedWeakCache, + cacheSizes, + defaultCacheSizes, +} from "../caching/index.js"; -let printCache!: WeakCache; +let printCache!: AutoCleanedWeakCache; export const print = Object.assign( (ast: ASTNode) => { let result = printCache.get(ast); @@ -15,10 +19,9 @@ export const print = Object.assign( }, { reset() { - printCache = new WeakCache< - ASTNode, - string - >(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); + printCache = new AutoCleanedWeakCache( + cacheSizes.print || defaultCacheSizes.print + ); }, } ); diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 35c1ec45cad..637ae100af7 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -131,3 +131,11 @@ export * from "./types/IsStrictlyAny.js"; export type { DeepOmit } from "./types/DeepOmit.js"; export type { DeepPartial } from "./types/DeepPartial.js"; export type { OnlyRequiredProperties } from "./types/OnlyRequiredProperties.js"; + +export { + AutoCleanedStrongCache, + AutoCleanedWeakCache, + cacheSizes, + defaultCacheSizes, +} from "./caching/index.js"; +export type { CacheSizes } from "./caching/index.js"; From fea00231e0e9cd77cdec46a671ea87c5a3f1f510 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 7 Dec 2023 22:15:55 +0100 Subject: [PATCH 51/90] Copy and use ApiDoc components from docs repo (#11416) --- .prettierignore | 5 + docs/shared/ApiDoc/Context.js | 6 + docs/shared/ApiDoc/DocBlock.js | 149 +++++++++++++++++++ docs/shared/ApiDoc/Function.js | 70 +++++++++ docs/shared/ApiDoc/Heading.js | 67 +++++++++ docs/shared/ApiDoc/InterfaceDetails.js | 37 +++++ docs/shared/ApiDoc/ParameterTable.js | 93 ++++++++++++ docs/shared/ApiDoc/PropertySignatureTable.js | 116 +++++++++++++++ docs/shared/ApiDoc/ResponsiveGrid.js | 77 ++++++++++ docs/shared/ApiDoc/index.js | 14 ++ docs/shared/ApiDoc/mdToReact.js | 20 +++ docs/source/api/core/ApolloClient.mdx | 39 ++--- netlify.toml | 2 +- 13 files changed, 676 insertions(+), 19 deletions(-) create mode 100644 docs/shared/ApiDoc/Context.js create mode 100644 docs/shared/ApiDoc/DocBlock.js create mode 100644 docs/shared/ApiDoc/Function.js create mode 100644 docs/shared/ApiDoc/Heading.js create mode 100644 docs/shared/ApiDoc/InterfaceDetails.js create mode 100644 docs/shared/ApiDoc/ParameterTable.js create mode 100644 docs/shared/ApiDoc/PropertySignatureTable.js create mode 100644 docs/shared/ApiDoc/ResponsiveGrid.js create mode 100644 docs/shared/ApiDoc/index.js create mode 100644 docs/shared/ApiDoc/mdToReact.js diff --git a/.prettierignore b/.prettierignore index 156d707c16e..d1ab5b159ad 100644 --- a/.prettierignore +++ b/.prettierignore @@ -25,6 +25,11 @@ /docs/source/development-testing/** !/docs/source/development-testing/reducing-bundle-size.mdx +!docs/shared +/docs/shared/** +!/docs/shared/ApiDoc +!/docs/shared/ApiDoc/** + node_modules/ .yalc/ .next/ diff --git a/docs/shared/ApiDoc/Context.js b/docs/shared/ApiDoc/Context.js new file mode 100644 index 00000000000..c6861650c92 --- /dev/null +++ b/docs/shared/ApiDoc/Context.js @@ -0,0 +1,6 @@ +import { useMDXComponents } from "@mdx-js/react"; + +export const useApiDocContext = function () { + const MDX = useMDXComponents(); + return MDX.useApiDocContext(this, arguments); +}; diff --git a/docs/shared/ApiDoc/DocBlock.js b/docs/shared/ApiDoc/DocBlock.js new file mode 100644 index 00000000000..333bda75afd --- /dev/null +++ b/docs/shared/ApiDoc/DocBlock.js @@ -0,0 +1,149 @@ +import PropTypes from "prop-types"; +import React from "react"; +import { Stack } from "@chakra-ui/react"; +import { mdToReact } from "./mdToReact"; +import { useApiDocContext } from "."; + +export function DocBlock({ + canonicalReference, + summary = true, + remarks = false, + example = false, + remarkCollapsible = true, + since = true, + deprecated = true, +}) { + return ( + + {/** TODO: @since, @deprecated etc. */} + {deprecated && } + {since && } + {summary && } + {remarks && ( + + )} + {example && } + + ); +} + +DocBlock.propTypes = { + canonicalReference: PropTypes.string.isRequired, + summary: PropTypes.bool, + remarks: PropTypes.bool, + example: PropTypes.bool, + remarkCollapsible: PropTypes.bool, + since: PropTypes.bool, + deprecated: PropTypes.bool, +}; + +function MaybeCollapsible({ collapsible, children }) { + return ( + collapsible ? + children ? +
+ Read more... + {children} +
+ : null + : children + ); +} +MaybeCollapsible.propTypes = { + collapsible: PropTypes.bool, + children: PropTypes.node, +}; + +/** + * Might still need more work on the Gatsby side to get this to work. + */ +export function Deprecated({ canonicalReference, collapsible = false }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + const value = item.comment?.deprecated; + if (!value) return null; + return ( + + {mdToReact(value)} + + ); +} +Deprecated.propTypes = { + canonicalReference: PropTypes.string.isRequired, + collapsible: PropTypes.bool, +}; + +/** + * Might still need more work on the Gatsby side to get this to work. + */ +export function Since({ canonicalReference, collapsible = false }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + const value = item.comment?.since; + if (!value) return null; + return ( + + Added to Apollo Client in version {value} + + ); +} +Since.propTypes = { + canonicalReference: PropTypes.string.isRequired, + collapsible: PropTypes.bool, +}; + +export function Summary({ canonicalReference, collapsible = false }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + const value = item.comment?.summary; + if (!value) return null; + return ( + + {mdToReact(value)} + + ); +} +Summary.propTypes = { + canonicalReference: PropTypes.string.isRequired, + collapsible: PropTypes.bool, +}; + +export function Remarks({ canonicalReference, collapsible = false }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + const value = item.comment?.remarks?.replace(/^@remarks/g, ""); + if (!value) return null; + return ( + + {mdToReact(value)} + + ); +} +Remarks.propTypes = { + canonicalReference: PropTypes.string.isRequired, + collapsible: PropTypes.bool, +}; + +export function Example({ + canonicalReference, + collapsible = false, + index = 0, +}) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + const value = item.comment?.examples[index]; + if (!value) return null; + return ( + + {mdToReact(value)} + + ); +} +Example.propTypes = { + canonicalReference: PropTypes.string.isRequired, + collapsible: PropTypes.bool, + index: PropTypes.number, +}; diff --git a/docs/shared/ApiDoc/Function.js b/docs/shared/ApiDoc/Function.js new file mode 100644 index 00000000000..97cb0934ce8 --- /dev/null +++ b/docs/shared/ApiDoc/Function.js @@ -0,0 +1,70 @@ +import PropTypes from "prop-types"; +import React from "react"; +import { ApiDocHeading, DocBlock, ParameterTable, useApiDocContext } from "."; + +export function FunctionSignature({ + canonicalReference, + parameterTypes = false, + name = true, + arrow = false, +}) { + const getItem = useApiDocContext(); + const { displayName, parameters, returnType } = getItem(canonicalReference); + + return ( + <> + {name ? displayName : ""}( + {parameters + .map((p) => { + let pStr = p.name; + if (p.optional) { + pStr += "?"; + } + if (parameterTypes) { + pStr += ": " + p.type; + } + return pStr; + }) + .join(", ")} + ){arrow ? " =>" : ":"} {returnType} + + ); +} + +FunctionSignature.propTypes = { + canonicalReference: PropTypes.string.isRequired, + parameterTypes: PropTypes.bool, + name: PropTypes.bool, + arrow: PropTypes.bool, +}; + +export function FunctionDetails({ + canonicalReference, + customParameterOrder, + headingLevel, +}) { + return ( + <> + + + + + ); +} + +FunctionDetails.propTypes = { + canonicalReference: PropTypes.string.isRequired, + headingLevel: PropTypes.number.isRequired, + customParameterOrder: PropTypes.arrayOf(PropTypes.string), +}; diff --git a/docs/shared/ApiDoc/Heading.js b/docs/shared/ApiDoc/Heading.js new file mode 100644 index 00000000000..e4a5aa9db69 --- /dev/null +++ b/docs/shared/ApiDoc/Heading.js @@ -0,0 +1,67 @@ +import { useMDXComponents } from "@mdx-js/react"; +import PropTypes from "prop-types"; +import React from "react"; +import { Box, Heading } from "@chakra-ui/react"; +import { FunctionSignature } from "."; +import { useApiDocContext } from "./Context"; + +const levels = { + 2: "xl", + 3: "lg", + 4: "md", + 5: "sm", + 6: "xs", +}; + +export function ApiDocHeading({ + canonicalReference, + headingLevel, + link = true, +}) { + const MDX = useMDXComponents(); + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + const heading = + ( + item.kind === "MethodSignature" || + item.kind === "Function" || + item.kind === "Method" + ) ? + + : item.displayName; + return ( + + + {link ? + + {heading} + + : heading} + + {item.file && ( + + + ({item.file}) + + + )} + + ); +} +ApiDocHeading.propTypes = { + canonicalReference: PropTypes.string.isRequired, + headingLevel: PropTypes.number.isRequired, + link: PropTypes.bool, +}; diff --git a/docs/shared/ApiDoc/InterfaceDetails.js b/docs/shared/ApiDoc/InterfaceDetails.js new file mode 100644 index 00000000000..e4b439181c4 --- /dev/null +++ b/docs/shared/ApiDoc/InterfaceDetails.js @@ -0,0 +1,37 @@ +import PropTypes from "prop-types"; +import React from "react"; +import { ApiDocHeading, DocBlock, PropertySignatureTable } from "."; +export function InterfaceDetails({ + canonicalReference, + headingLevel, + link, + customPropertyOrder, +}) { + return ( + <> + + + + + ); +} + +InterfaceDetails.propTypes = { + canonicalReference: PropTypes.string.isRequired, + headingLevel: PropTypes.number.isRequired, + link: PropTypes.bool, + customPropertyOrder: PropTypes.arrayOf(PropTypes.string), +}; diff --git a/docs/shared/ApiDoc/ParameterTable.js b/docs/shared/ApiDoc/ParameterTable.js new file mode 100644 index 00000000000..49f2a7abf9c --- /dev/null +++ b/docs/shared/ApiDoc/ParameterTable.js @@ -0,0 +1,93 @@ +import { useMDXComponents } from "@mdx-js/react"; + +import PropTypes from "prop-types"; +import React from "react"; +import { GridItem, chakra } from "@chakra-ui/react"; +import { PropertySignatureTable, useApiDocContext } from "."; +import { ResponsiveGrid } from "./ResponsiveGrid"; +import { mdToReact } from "./mdToReact"; + +export function ParameterTable({ canonicalReference }) { + const MDX = useMDXComponents(); + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + + if (item.parameters.length === 0) return null; + + return ( + <> + + + Parameters + + + + Name / Type + Description + + {item.parameters.map((parameter) => { + const baseType = parameter.type.split("<")[0]; + const reference = getItem( + item.references?.find((r) => r.text === baseType) + ?.canonicalReference, + false + ); + const interfaceReference = + reference?.kind === "Interface" ? reference : null; + + return ( + + + + {parameter.name} + {parameter.optional ? + (optional) + : null} + + + {parameter.type} + + + + {mdToReact(parameter.comment)} + + {interfaceReference && ( +
+ + Show/hide child attributes + + +
+ )} +
+ ); + })} +
+ + ); +} + +ParameterTable.propTypes = { + canonicalReference: PropTypes.string.isRequired, +}; diff --git a/docs/shared/ApiDoc/PropertySignatureTable.js b/docs/shared/ApiDoc/PropertySignatureTable.js new file mode 100644 index 00000000000..317b2926f0f --- /dev/null +++ b/docs/shared/ApiDoc/PropertySignatureTable.js @@ -0,0 +1,116 @@ +import { useMDXComponents } from "@mdx-js/react"; + +import PropTypes from "prop-types"; +import React, { useMemo } from "react"; +import { DocBlock, FunctionSignature, useApiDocContext } from "."; +import { GridItem, Text, chakra } from "@chakra-ui/react"; +import { ResponsiveGrid } from "./ResponsiveGrid"; + +export function PropertySignatureTable({ + canonicalReference, + prefix = "", + showHeaders = true, + display = "parent", + customOrder = [], +}) { + const MDX = useMDXComponents(); + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + const Wrapper = display === "parent" ? ResponsiveGrid : React.Fragment; + + const sortedProperties = useMemo( + () => + item.properties.map(getItem).sort((a, b) => { + const aIndex = customOrder.indexOf(a.displayName); + const bIndex = customOrder.indexOf(b.displayName); + if (aIndex >= 0 && bIndex >= 0) { + return aIndex - bIndex; + } else if (aIndex >= 0) { + return -1; + } else if (bIndex >= 0) { + return 1; + } else { + return a.displayName.localeCompare(b.displayName); + } + }), + [item.properties, getItem, customOrder] + ); + + return ( + <> + {showHeaders ? + + + Properties + + + : null} + {item.childrenIncomplete ? + +
+ (Warning: some properties might be missing from the table due to + complex inheritance!) +
+ : null} + + + {showHeaders ? + <> + Name / Type + Description + + : null} + + {sortedProperties.map((property) => ( + + + + + + {prefix} + + {property.displayName} + + {property.optional ? + (optional) + : null} + + + {property.kind === "MethodSignature" ? + + : property.type} + + + + + + + ))} + + + ); +} + +PropertySignatureTable.propTypes = { + canonicalReference: PropTypes.string.isRequired, + prefix: PropTypes.string, + showHeaders: PropTypes.bool, + display: PropTypes.oneOf(["parent", "child"]), + customOrder: PropTypes.arrayOf(PropTypes.string), +}; diff --git a/docs/shared/ApiDoc/ResponsiveGrid.js b/docs/shared/ApiDoc/ResponsiveGrid.js new file mode 100644 index 00000000000..691a4afebcf --- /dev/null +++ b/docs/shared/ApiDoc/ResponsiveGrid.js @@ -0,0 +1,77 @@ +import React from "react"; +import { Global } from "@emotion/react"; +import { Grid } from "@chakra-ui/react"; + +/** + * This component is actually over in the docs repo, just repeated here so the + * styles are visible. + */ +export function ResponsiveGridStyles() { + return ( + *, .responsive-grid > details > *": { + background: "var(--chakra-colors-bg)", + }, + ".responsive-grid .cell, .responsive-grid .row": { + padding: "var(--chakra-space-4)", + }, + ".responsive-grid details .first.cell + .cell": { + marginTop: -1, + paddingTop: 0, + }, + ".responsive-grid details h6": { + display: "inline", + }, + ".responsive-grid .heading": { + fontFamily: "var(--chakra-fonts-heading)", + fontWeight: "var(--chakra-fontWeights-normal)", + textTransform: "uppercase", + letterSpacing: "var(--chakra-letterSpacings-wider)", + fontSize: "var(--chakra-fontSizes-xs)", + }, + }} + /> + ); +} + +export function ResponsiveGrid({ children }) { + /* + responsiveness not regarding screen width, but actual available space: + if less than 350px, show only one column + show at most two columns (that's where the 45% hack comes - 45% * 3 won't fit) + */ + + return ( + + {children} + + ); +} diff --git a/docs/shared/ApiDoc/index.js b/docs/shared/ApiDoc/index.js new file mode 100644 index 00000000000..66bfa413afa --- /dev/null +++ b/docs/shared/ApiDoc/index.js @@ -0,0 +1,14 @@ +export { useApiDocContext } from "./Context"; +export { + DocBlock, + Deprecated, + Example, + Remarks, + Since, + Summary, +} from "./DocBlock"; +export { PropertySignatureTable } from "./PropertySignatureTable"; +export { ApiDocHeading } from "./Heading"; +export { InterfaceDetails } from "./InterfaceDetails"; +export { FunctionSignature, FunctionDetails } from "./Function"; +export { ParameterTable } from "./ParameterTable"; diff --git a/docs/shared/ApiDoc/mdToReact.js b/docs/shared/ApiDoc/mdToReact.js new file mode 100644 index 00000000000..307ca38c7bf --- /dev/null +++ b/docs/shared/ApiDoc/mdToReact.js @@ -0,0 +1,20 @@ +import PropTypes from "prop-types"; +import React from "react"; +import ReactMarkdown from "react-markdown"; +import { useMDXComponents } from "@mdx-js/react"; + +export function mdToReact(text) { + const sanitized = text + .replace(/\{@link (\w*)\}/g, "[$1](#$1)") + .replace(//g, ""); + return ; +} + +function RenderMd({ markdown }) { + return ( + {markdown} + ); +} +RenderMd.propTypes = { + markdown: PropTypes.string.isRequired, +}; diff --git a/docs/source/api/core/ApolloClient.mdx b/docs/source/api/core/ApolloClient.mdx index 8795e1d3d88..a66e5713d6d 100644 --- a/docs/source/api/core/ApolloClient.mdx +++ b/docs/source/api/core/ApolloClient.mdx @@ -8,6 +8,8 @@ api_doc: - "@apollo/client!DefaultOptions:interface" --- +import { InterfaceDetails, FunctionDetails, DocBlock, Example } from '../../../shared/ApiDoc'; + The `ApolloClient` class encapsulates Apollo's core client-side API. It backs all available view-layer integrations (React, iOS, and so on). ## The `ApolloClient` constructor @@ -20,33 +22,34 @@ Returns an initialized `ApolloClient` object. #### Example - + For more information on the `defaultOptions` object, see the [Default Options](#DefaultOptions) section below. ## Functions - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + ## Types - + ##### Example `defaultOptions` object diff --git a/netlify.toml b/netlify.toml index 67879a8b0ba..0dc401e05ad 100644 --- a/netlify.toml +++ b/netlify.toml @@ -13,7 +13,7 @@ npm run docmodel cd ../ rm -rf monodocs - git clone https://github.com/apollographql/docs --branch main --single-branch monodocs + git clone https://github.com/apollographql/docs --branch pr/expose-hook --single-branch monodocs cd monodocs npm i cp -r ../docs local From efeea04a39b206ebcd14995ed096ef8224665c61 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 8 Dec 2023 10:37:24 +0100 Subject: [PATCH 52/90] Update netlify.toml Change deploy preview branch back to `main` --- netlify.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netlify.toml b/netlify.toml index 0dc401e05ad..67879a8b0ba 100644 --- a/netlify.toml +++ b/netlify.toml @@ -13,7 +13,7 @@ npm run docmodel cd ../ rm -rf monodocs - git clone https://github.com/apollographql/docs --branch pr/expose-hook --single-branch monodocs + git clone https://github.com/apollographql/docs --branch main --single-branch monodocs cd monodocs npm i cp -r ../docs local From 865659b879dc1458812df6718fdeb72f211ad496 Mon Sep 17 00:00:00 2001 From: Mohit <36567063+mohit23x@users.noreply.github.com> Date: Fri, 8 Dec 2023 19:14:09 +0530 Subject: [PATCH 53/90] Update react-native.md - Adds react native devtool supported by flipper (#11364) Co-authored-by: Lenz Weber-Tronic --- .changeset/fuzzy-eyes-lick.md | 5 ++++ docs/source/integrations/react-native.md | 32 ++++++++++++++++++++---- 2 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 .changeset/fuzzy-eyes-lick.md diff --git a/.changeset/fuzzy-eyes-lick.md b/.changeset/fuzzy-eyes-lick.md new file mode 100644 index 00000000000..c69c857fd51 --- /dev/null +++ b/.changeset/fuzzy-eyes-lick.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Update react-native.md - Adds react native devtool in the documentation diff --git a/docs/source/integrations/react-native.md b/docs/source/integrations/react-native.md index 68dfdd23bf6..b98605641d9 100644 --- a/docs/source/integrations/react-native.md +++ b/docs/source/integrations/react-native.md @@ -38,11 +38,33 @@ For more information on setting up Apollo Client, see [Getting started](../get-s ## Apollo Client Devtools -[React Native Debugger](https://github.com/jhen0409/react-native-debugger) supports the [Apollo Client Devtools](../development-testing/developer-tooling/#apollo-client-devtools): - -1. Install React Native Debugger and open it. -2. Enable "Debug JS Remotely" in your app. -3. If you don't see the Developer Tools panel or the Apollo tab is missing from it, toggle the Developer Tools by right-clicking anywhere and selecting **Toggle Developer Tools**. +#### 1. Using [React Native Debugger](https://github.com/jhen0409/react-native-debugger) + +The React Native Debugger supports the [Apollo Client Devtools](../development-testing/developer-tooling/#apollo-client-devtools): + + 1. Install React Native Debugger and open it. + 2. Enable "Debug JS Remotely" in your app. + 3. If you don't see the Developer Tools panel or the Apollo tab is missing from it, toggle the Developer Tools by right-clicking anywhere and selecting **Toggle Developer Tools**. + +#### 2. Using [Flipper](https://fbflipper.com/) + +A community plugin called [React Native Apollo devtools](https://github.com/razorpay/react-native-apollo-devtools) is available for Flipper, which supports viewing cache data. + + 1. Install Flipper and open it. + 2. Go to add plugin and search for `react-native-apollo-devtools` and install it + 3. Add `react-native-flipper` and `react-native-apollo-devtools-client` as dev dependecy to react native app. + 4. Initialize the plugin with flipper on client side + + ``` + import { apolloDevToolsInit } from 'react-native-apollo-devtools-client'; + const client = new ApolloClient({ + // ... + }) + + if(__DEV__){ + apolloDevToolsInit(client); + } + ``` ## Consuming multipart HTTP via text streaming From 98431c1ae25eeda65491618811fbc95b6b432bad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:27:35 -0500 Subject: [PATCH 54/90] chore(deps): update actions/stale action to v9 (#11423) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/close-stale-issues.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/close-stale-issues.yml b/.github/workflows/close-stale-issues.yml index 5d54d58ea6d..55a67e4ae69 100644 --- a/.github/workflows/close-stale-issues.yml +++ b/.github/workflows/close-stale-issues.yml @@ -20,7 +20,7 @@ jobs: steps: - name: Close Stale Issues - uses: actions/stale@v8.0.0 + uses: actions/stale@v9.0.0 with: # # Token for the repository. Can be passed in using `{{ secrets.GITHUB_TOKEN }}`. # repo-token: # optional, default is ${{ github.token }} From 13c27bb1e2f83380a34c031db703335893a4ddcd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 16:27:58 -0500 Subject: [PATCH 55/90] chore(deps): update cimg/node docker tag to v21.4.0 (#11422) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .circleci/config.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 23aead0e9ae..609041812f5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,7 +15,7 @@ jobs: Lint: docker: - - image: cimg/node:21.2.0 + - image: cimg/node:21.4.0 steps: - checkout - run: npm version @@ -24,7 +24,7 @@ jobs: Formatting: docker: - - image: cimg/node:21.2.0 + - image: cimg/node:21.4.0 steps: - checkout - run: npm ci @@ -32,7 +32,7 @@ jobs: Tests: docker: - - image: cimg/node:21.2.0 + - image: cimg/node:21.4.0 steps: - checkout - run: npm run ci:precheck @@ -50,7 +50,7 @@ jobs: BuildTarball: docker: - - image: cimg/node:21.2.0 + - image: cimg/node:21.4.0 steps: - checkout - run: npm run ci:precheck @@ -67,7 +67,7 @@ jobs: framework: type: string docker: - - image: cimg/node:21.2.0 + - image: cimg/node:21.4.0 steps: - checkout - attach_workspace: From aafcd3efc85da1fffe7e615e2e8c0baad90588b9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 08:55:16 -0500 Subject: [PATCH 56/90] chore(deps): update all devdependencies (#11406) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 382 ++++++++++++++++++++++------------------------ package.json | 18 +-- 2 files changed, 189 insertions(+), 211 deletions(-) diff --git a/package-lock.json b/package-lock.json index 71a75840fdf..c8a1b3868b5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,8 +28,8 @@ "devDependencies": { "@arethetypeswrong/cli": "0.13.2", "@babel/parser": "7.23.5", - "@changesets/changelog-github": "0.4.8", - "@changesets/cli": "2.26.2", + "@changesets/changelog-github": "0.5.0", + "@changesets/cli": "2.27.1", "@graphql-tools/schema": "10.0.2", "@microsoft/api-extractor": "7.38.3", "@rollup/plugin-node-resolve": "11.2.1", @@ -52,16 +52,16 @@ "@types/react-dom": "18.2.17", "@types/relay-runtime": "14.1.14", "@types/use-sync-external-store": "0.0.6", - "@typescript-eslint/eslint-plugin": "6.12.0", - "@typescript-eslint/parser": "6.12.0", - "@typescript-eslint/rule-tester": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/utils": "6.12.0", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", + "@typescript-eslint/rule-tester": "6.14.0", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/utils": "6.14.0", "acorn": "8.11.2", "blob-polyfill": "7.0.20220408", "bytes": "3.1.2", "cross-fetch": "4.0.0", - "eslint": "8.54.0", + "eslint": "8.55.0", "eslint-import-resolver-typescript": "3.6.1", "eslint-plugin-import": "npm:@phryneas/eslint-plugin-import@2.27.5-pr.2813.2817.199971c", "eslint-plugin-local-rules": "2.0.1", @@ -91,7 +91,7 @@ "rxjs": "7.8.1", "size-limit": "11.0.0", "subscriptions-transport-ws": "0.11.0", - "terser": "5.24.0", + "terser": "5.26.0", "ts-api-utils": "1.0.3", "ts-jest": "29.1.1", "ts-jest-resolver": "2.0.1", @@ -799,16 +799,16 @@ "dev": true }, "node_modules/@changesets/apply-release-plan": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-6.1.4.tgz", - "integrity": "sha512-FMpKF1fRlJyCZVYHr3CbinpZZ+6MwvOtWUuO8uo+svcATEoc1zRDcj23pAurJ2TZ/uVz1wFHH6K3NlACy0PLew==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.0.tgz", + "integrity": "sha512-vfi69JR416qC9hWmFGSxj7N6wA5J222XNBmezSVATPWDVPIF7gkd4d8CpbEbXmRWbVrkoli3oerGS6dcL/BGsQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/config": "^2.3.1", - "@changesets/get-version-range-type": "^0.3.2", - "@changesets/git": "^2.0.0", - "@changesets/types": "^5.2.1", + "@changesets/config": "^3.0.0", + "@changesets/get-version-range-type": "^0.4.0", + "@changesets/git": "^3.0.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "detect-indent": "^6.0.0", "fs-extra": "^7.0.1", @@ -850,23 +850,23 @@ } }, "node_modules/@changesets/assemble-release-plan": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-5.2.4.tgz", - "integrity": "sha512-xJkWX+1/CUaOUWTguXEbCDTyWJFECEhmdtbkjhn5GVBGxdP/JwaHBIU9sW3FR6gD07UwZ7ovpiPclQZs+j+mvg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-6.0.0.tgz", + "integrity": "sha512-4QG7NuisAjisbW4hkLCmGW2lRYdPrKzro+fCtZaILX+3zdUELSvYjpL4GTv0E4aM9Mef3PuIQp89VmHJ4y2bfw==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/errors": "^0.1.4", - "@changesets/get-dependents-graph": "^1.3.6", - "@changesets/types": "^5.2.1", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.0.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "semver": "^7.5.3" } }, "node_modules/@changesets/assemble-release-plan/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -879,55 +879,54 @@ } }, "node_modules/@changesets/changelog-git": { - "version": "0.1.14", - "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.1.14.tgz", - "integrity": "sha512-+vRfnKtXVWsDDxGctOfzJsPhaCdXRYoe+KyWYoq5X/GqoISREiat0l3L8B0a453B2B4dfHGcZaGyowHbp9BSaA==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-0.2.0.tgz", + "integrity": "sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==", "dev": true, "dependencies": { - "@changesets/types": "^5.2.1" + "@changesets/types": "^6.0.0" } }, "node_modules/@changesets/changelog-github": { - "version": "0.4.8", - "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.4.8.tgz", - "integrity": "sha512-jR1DHibkMAb5v/8ym77E4AMNWZKB5NPzw5a5Wtqm1JepAuIF+hrKp2u04NKM14oBZhHglkCfrla9uq8ORnK/dw==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-github/-/changelog-github-0.5.0.tgz", + "integrity": "sha512-zoeq2LJJVcPJcIotHRJEEA2qCqX0AQIeFE+L21L8sRLPVqDhSXY8ZWAt2sohtBpFZkBwu+LUwMSKRr2lMy3LJA==", "dev": true, "dependencies": { - "@changesets/get-github-info": "^0.5.2", - "@changesets/types": "^5.2.1", + "@changesets/get-github-info": "^0.6.0", + "@changesets/types": "^6.0.0", "dotenv": "^8.1.0" } }, "node_modules/@changesets/cli": { - "version": "2.26.2", - "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.26.2.tgz", - "integrity": "sha512-dnWrJTmRR8bCHikJHl9b9HW3gXACCehz4OasrXpMp7sx97ECuBGGNjJhjPhdZNCvMy9mn4BWdplI323IbqsRig==", + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-2.27.1.tgz", + "integrity": "sha512-iJ91xlvRnnrJnELTp4eJJEOPjgpF3NOh4qeQehM6Ugiz9gJPRZ2t+TsXun6E3AMN4hScZKjqVXl0TX+C7AB3ZQ==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/apply-release-plan": "^6.1.4", - "@changesets/assemble-release-plan": "^5.2.4", - "@changesets/changelog-git": "^0.1.14", - "@changesets/config": "^2.3.1", - "@changesets/errors": "^0.1.4", - "@changesets/get-dependents-graph": "^1.3.6", - "@changesets/get-release-plan": "^3.0.17", - "@changesets/git": "^2.0.0", - "@changesets/logger": "^0.0.5", - "@changesets/pre": "^1.0.14", - "@changesets/read": "^0.5.9", - "@changesets/types": "^5.2.1", - "@changesets/write": "^0.2.3", + "@changesets/apply-release-plan": "^7.0.0", + "@changesets/assemble-release-plan": "^6.0.0", + "@changesets/changelog-git": "^0.2.0", + "@changesets/config": "^3.0.0", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.0.0", + "@changesets/get-release-plan": "^4.0.0", + "@changesets/git": "^3.0.0", + "@changesets/logger": "^0.1.0", + "@changesets/pre": "^2.0.0", + "@changesets/read": "^0.6.0", + "@changesets/types": "^6.0.0", + "@changesets/write": "^0.3.0", "@manypkg/get-packages": "^1.1.3", - "@types/is-ci": "^3.0.0", "@types/semver": "^7.5.0", "ansi-colors": "^4.1.3", "chalk": "^2.1.0", + "ci-info": "^3.7.0", "enquirer": "^2.3.0", "external-editor": "^3.1.0", "fs-extra": "^7.0.1", "human-id": "^1.0.2", - "is-ci": "^3.0.1", "meow": "^6.0.0", "outdent": "^0.5.0", "p-limit": "^2.2.0", @@ -966,9 +965,9 @@ } }, "node_modules/@changesets/cli/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -993,36 +992,36 @@ } }, "node_modules/@changesets/config": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@changesets/config/-/config-2.3.1.tgz", - "integrity": "sha512-PQXaJl82CfIXddUOppj4zWu+987GCw2M+eQcOepxN5s+kvnsZOwjEJO3DH9eVy+OP6Pg/KFEWdsECFEYTtbg6w==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-3.0.0.tgz", + "integrity": "sha512-o/rwLNnAo/+j9Yvw9mkBQOZySDYyOr/q+wptRLcAVGlU6djOeP9v1nlalbL9MFsobuBVQbZCTp+dIzdq+CLQUA==", "dev": true, "dependencies": { - "@changesets/errors": "^0.1.4", - "@changesets/get-dependents-graph": "^1.3.6", - "@changesets/logger": "^0.0.5", - "@changesets/types": "^5.2.1", + "@changesets/errors": "^0.2.0", + "@changesets/get-dependents-graph": "^2.0.0", + "@changesets/logger": "^0.1.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1", "micromatch": "^4.0.2" } }, "node_modules/@changesets/errors": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.1.4.tgz", - "integrity": "sha512-HAcqPF7snsUJ/QzkWoKfRfXushHTu+K5KZLJWPb34s4eCZShIf8BFO3fwq6KU8+G7L5KdtN2BzQAXOSXEyiY9Q==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-0.2.0.tgz", + "integrity": "sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==", "dev": true, "dependencies": { "extendable-error": "^0.1.5" } }, "node_modules/@changesets/get-dependents-graph": { - "version": "1.3.6", - "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.3.6.tgz", - "integrity": "sha512-Q/sLgBANmkvUm09GgRsAvEtY3p1/5OCzgBE5vX3vgb5CvW0j7CEljocx5oPXeQSNph6FXulJlXV3Re/v3K3P3Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-2.0.0.tgz", + "integrity": "sha512-cafUXponivK4vBgZ3yLu944mTvam06XEn2IZGjjKc0antpenkYANXiiE6GExV/yKdsCnE8dXVZ25yGqLYZmScA==", "dev": true, "dependencies": { - "@changesets/types": "^5.2.1", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "chalk": "^2.1.0", "fs-extra": "^7.0.1", @@ -1053,9 +1052,9 @@ } }, "node_modules/@changesets/get-dependents-graph/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1080,9 +1079,9 @@ } }, "node_modules/@changesets/get-github-info": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.5.2.tgz", - "integrity": "sha512-JppheLu7S114aEs157fOZDjFqUDpm7eHdq5E8SSR0gUBTEK0cNSHsrSR5a66xs0z3RWuo46QvA3vawp8BxDHvg==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@changesets/get-github-info/-/get-github-info-0.6.0.tgz", + "integrity": "sha512-v/TSnFVXI8vzX9/w3DU2Ol+UlTZcu3m0kXTjTT4KlAdwSvwutcByYwyYn9hwerPWfPkT2JfpoX0KgvCEi8Q/SA==", "dev": true, "dependencies": { "dataloader": "^1.4.0", @@ -1090,35 +1089,35 @@ } }, "node_modules/@changesets/get-release-plan": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-3.0.17.tgz", - "integrity": "sha512-6IwKTubNEgoOZwDontYc2x2cWXfr6IKxP3IhKeK+WjyD6y3M4Gl/jdQvBw+m/5zWILSOCAaGLu2ZF6Q+WiPniw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-release-plan/-/get-release-plan-4.0.0.tgz", + "integrity": "sha512-9L9xCUeD/Tb6L/oKmpm8nyzsOzhdNBBbt/ZNcjynbHC07WW4E1eX8NMGC5g5SbM5z/V+MOrYsJ4lRW41GCbg3w==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/assemble-release-plan": "^5.2.4", - "@changesets/config": "^2.3.1", - "@changesets/pre": "^1.0.14", - "@changesets/read": "^0.5.9", - "@changesets/types": "^5.2.1", + "@changesets/assemble-release-plan": "^6.0.0", + "@changesets/config": "^3.0.0", + "@changesets/pre": "^2.0.0", + "@changesets/read": "^0.6.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3" } }, "node_modules/@changesets/get-version-range-type": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.3.2.tgz", - "integrity": "sha512-SVqwYs5pULYjYT4op21F2pVbcrca4qA/bAA3FmFXKMN7Y+HcO8sbZUTx3TAy2VXulP2FACd1aC7f2nTuqSPbqg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.4.0.tgz", + "integrity": "sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==", "dev": true }, "node_modules/@changesets/git": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@changesets/git/-/git-2.0.0.tgz", - "integrity": "sha512-enUVEWbiqUTxqSnmesyJGWfzd51PY4H7mH9yUw0hPVpZBJ6tQZFMU3F3mT/t9OJ/GjyiM4770i+sehAn6ymx6A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-3.0.0.tgz", + "integrity": "sha512-vvhnZDHe2eiBNRFHEgMiGd2CT+164dfYyrJDhwwxTVD/OW0FUD6G7+4DIx1dNwkwjHyzisxGAU96q0sVNBns0w==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/errors": "^0.1.4", - "@changesets/types": "^5.2.1", + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "is-subdir": "^1.1.1", "micromatch": "^4.0.2", @@ -1126,9 +1125,9 @@ } }, "node_modules/@changesets/logger": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.0.5.tgz", - "integrity": "sha512-gJyZHomu8nASHpaANzc6bkQMO9gU/ib20lqew1rVx753FOxffnCrJlGIeQVxNWCqM+o6OOleCo/ivL8UAO5iFw==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@changesets/logger/-/logger-0.1.0.tgz", + "integrity": "sha512-pBrJm4CQm9VqFVwWnSqKEfsS2ESnwqwH+xR7jETxIErZcfd1u2zBSqrHbRHR7xjhSgep9x2PSKFKY//FAshA3g==", "dev": true, "dependencies": { "chalk": "^2.1.0" @@ -1170,39 +1169,39 @@ } }, "node_modules/@changesets/parse": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.16.tgz", - "integrity": "sha512-127JKNd167ayAuBjUggZBkmDS5fIKsthnr9jr6bdnuUljroiERW7FBTDNnNVyJ4l69PzR57pk6mXQdtJyBCJKg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-0.4.0.tgz", + "integrity": "sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==", "dev": true, "dependencies": { - "@changesets/types": "^5.2.1", + "@changesets/types": "^6.0.0", "js-yaml": "^3.13.1" } }, "node_modules/@changesets/pre": { - "version": "1.0.14", - "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-1.0.14.tgz", - "integrity": "sha512-dTsHmxQWEQekHYHbg+M1mDVYFvegDh9j/kySNuDKdylwfMEevTeDouR7IfHNyVodxZXu17sXoJuf2D0vi55FHQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-2.0.0.tgz", + "integrity": "sha512-HLTNYX/A4jZxc+Sq8D1AMBsv+1qD6rmmJtjsCJa/9MSRybdxh0mjbTvE6JYZQ/ZiQ0mMlDOlGPXTm9KLTU3jyw==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/errors": "^0.1.4", - "@changesets/types": "^5.2.1", + "@changesets/errors": "^0.2.0", + "@changesets/types": "^6.0.0", "@manypkg/get-packages": "^1.1.3", "fs-extra": "^7.0.1" } }, "node_modules/@changesets/read": { - "version": "0.5.9", - "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.5.9.tgz", - "integrity": "sha512-T8BJ6JS6j1gfO1HFq50kU3qawYxa4NTbI/ASNVVCBTsKquy2HYwM9r7ZnzkiMe8IEObAJtUVGSrePCOxAK2haQ==", + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-0.6.0.tgz", + "integrity": "sha512-ZypqX8+/im1Fm98K4YcZtmLKgjs1kDQ5zHpc2U1qdtNBmZZfo/IBiG162RoP0CUF05tvp2y4IspH11PLnPxuuw==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/git": "^2.0.0", - "@changesets/logger": "^0.0.5", - "@changesets/parse": "^0.3.16", - "@changesets/types": "^5.2.1", + "@changesets/git": "^3.0.0", + "@changesets/logger": "^0.1.0", + "@changesets/parse": "^0.4.0", + "@changesets/types": "^6.0.0", "chalk": "^2.1.0", "fs-extra": "^7.0.1", "p-filter": "^2.1.0" @@ -1244,19 +1243,19 @@ } }, "node_modules/@changesets/types": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@changesets/types/-/types-5.2.1.tgz", - "integrity": "sha512-myLfHbVOqaq9UtUKqR/nZA/OY7xFjQMdfgfqeZIBK4d0hA6pgxArvdv8M+6NUzzBsjWLOtvApv8YHr4qM+Kpfg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-6.0.0.tgz", + "integrity": "sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==", "dev": true }, "node_modules/@changesets/write": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.2.3.tgz", - "integrity": "sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-0.3.0.tgz", + "integrity": "sha512-slGLb21fxZVUYbyea+94uFiD6ntQW0M2hIKNznFizDhZPDgn2c/fv1UzzlW43RVzh1BEDuIqW6hzlJ1OflNmcw==", "dev": true, "dependencies": { "@babel/runtime": "^7.20.1", - "@changesets/types": "^5.2.1", + "@changesets/types": "^6.0.0", "fs-extra": "^7.0.1", "human-id": "^1.0.2", "prettier": "^2.7.1" @@ -1676,9 +1675,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.3.tgz", - "integrity": "sha512-yZzuIG+jnVu6hNSzFEN07e8BxF3uAzYtQb6uDkaYZLo6oYZDCq454c5kB8zxnzfCYyP4MIuyBn10L0DqwujTmA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", @@ -1744,9 +1743,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", - "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz", + "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -3206,15 +3205,6 @@ "hoist-non-react-statics": "^3.3.0" } }, - "node_modules/@types/is-ci": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.0.tgz", - "integrity": "sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==", - "dev": true, - "dependencies": { - "ci-info": "^3.1.0" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -3434,16 +3424,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz", - "integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.14.0.tgz", + "integrity": "sha512-1ZJBykBCXaSHG94vMMKmiHoL0MhNHKSVlcHVYZNw+BKxufhqQVTOawNpwwI1P5nIFZ/4jLVop0mcY6mJJDFNaw==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/type-utils": "6.12.0", - "@typescript-eslint/utils": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.14.0", + "@typescript-eslint/type-utils": "6.14.0", + "@typescript-eslint/utils": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -3484,15 +3474,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz", - "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.14.0.tgz", + "integrity": "sha512-QjToC14CKacd4Pa7JK4GeB/vHmWFJckec49FR4hmIRf97+KXole0T97xxu9IFiPxVQ1DBWrQ5wreLwAGwWAVQA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/scope-manager": "6.14.0", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/typescript-estree": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0", "debug": "^4.3.4" }, "engines": { @@ -3512,13 +3502,13 @@ } }, "node_modules/@typescript-eslint/rule-tester": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-6.12.0.tgz", - "integrity": "sha512-O1kFPAuX9H63GNDTyd8GKO5RioxRX96mAVcevbUywVtkrp8eoVLEf2VmKIKCeYAM5oZst52DMVCQXjcQuyxq5w==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/rule-tester/-/rule-tester-6.14.0.tgz", + "integrity": "sha512-SCxrm68pudpNTUGCqaGxgqNujca+sEjJXA52gH3b0q1DrVBq1VSlxgO9kSmodhbXKVyS7UGlbz3dPDqa65gR2g==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/utils": "6.12.0", + "@typescript-eslint/typescript-estree": "6.14.0", + "@typescript-eslint/utils": "6.14.0", "ajv": "^6.10.0", "lodash.merge": "4.6.2", "semver": "^7.5.4" @@ -3551,13 +3541,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz", - "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.14.0.tgz", + "integrity": "sha512-VT7CFWHbZipPncAZtuALr9y3EuzY1b1t1AEkIq2bTXUPKw+pHoXflGNG5L+Gv6nKul1cz1VH8fz16IThIU0tdg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0" + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3568,13 +3558,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz", - "integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.14.0.tgz", + "integrity": "sha512-x6OC9Q7HfYKqjnuNu5a7kffIYs3No30isapRBJl1iCHLitD8O0lFbRcVGiOcuyN837fqXzPZ1NS10maQzZMKqw==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.12.0", - "@typescript-eslint/utils": "6.12.0", + "@typescript-eslint/typescript-estree": "6.14.0", + "@typescript-eslint/utils": "6.14.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -3595,9 +3585,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz", - "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.14.0.tgz", + "integrity": "sha512-uty9H2K4Xs8E47z3SnXEPRNDfsis8JO27amp2GNCnzGETEW3yTqEIVg5+AI7U276oGF/tw6ZA+UesxeQ104ceA==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -3608,13 +3598,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz", - "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.14.0.tgz", + "integrity": "sha512-yPkaLwK0yH2mZKFE/bXkPAkkFgOv15GJAUzgUVonAbv0Hr4PK/N2yaA/4XQbTZQdygiDkpt5DkxPELqHguNvyw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/visitor-keys": "6.12.0", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/visitor-keys": "6.14.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -3650,17 +3640,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz", - "integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.14.0.tgz", + "integrity": "sha512-XwRTnbvRr7Ey9a1NT6jqdKX8y/atWG+8fAIu3z73HSP8h06i3r/ClMhmaF/RGWGW1tHJEwij1uEg2GbEmPYvYg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/typescript-estree": "6.12.0", + "@typescript-eslint/scope-manager": "6.14.0", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/typescript-estree": "6.14.0", "semver": "^7.5.4" }, "engines": { @@ -3690,12 +3680,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz", - "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.14.0.tgz", + "integrity": "sha512-fB5cw6GRhJUz03MrROVuj5Zm/Q+XWlVdIsFj+Zb1Hvqouc8t+XP2H5y53QYU/MGtd2dPg6/vJJlhoX3xc2ehfw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/types": "6.14.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -5581,15 +5571,15 @@ } }, "node_modules/eslint": { - "version": "8.54.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", - "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", + "version": "8.55.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz", + "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.54.0", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.55.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -5993,9 +5983,9 @@ } }, "node_modules/eslint/node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -7228,18 +7218,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "dev": true, - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, "node_modules/is-core-module": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", @@ -11664,9 +11642,9 @@ } }, "node_modules/terser": { - "version": "5.24.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.24.0.tgz", - "integrity": "sha512-ZpGR4Hy3+wBEzVEnHvstMvqpD/nABNelQn/z2r0fjVWGQsN3bpOLzQlqDxmb4CDZnXq5lpjnQ+mHQLAOpfM5iw==", + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", diff --git a/package.json b/package.json index dc9e6941396..b98d0729d27 100644 --- a/package.json +++ b/package.json @@ -109,8 +109,8 @@ "devDependencies": { "@arethetypeswrong/cli": "0.13.2", "@babel/parser": "7.23.5", - "@changesets/changelog-github": "0.4.8", - "@changesets/cli": "2.26.2", + "@changesets/changelog-github": "0.5.0", + "@changesets/cli": "2.27.1", "@graphql-tools/schema": "10.0.2", "@microsoft/api-extractor": "7.38.3", "@rollup/plugin-node-resolve": "11.2.1", @@ -133,16 +133,16 @@ "@types/react-dom": "18.2.17", "@types/relay-runtime": "14.1.14", "@types/use-sync-external-store": "0.0.6", - "@typescript-eslint/eslint-plugin": "6.12.0", - "@typescript-eslint/parser": "6.12.0", - "@typescript-eslint/rule-tester": "6.12.0", - "@typescript-eslint/types": "6.12.0", - "@typescript-eslint/utils": "6.12.0", + "@typescript-eslint/eslint-plugin": "6.14.0", + "@typescript-eslint/parser": "6.14.0", + "@typescript-eslint/rule-tester": "6.14.0", + "@typescript-eslint/types": "6.14.0", + "@typescript-eslint/utils": "6.14.0", "acorn": "8.11.2", "blob-polyfill": "7.0.20220408", "bytes": "3.1.2", "cross-fetch": "4.0.0", - "eslint": "8.54.0", + "eslint": "8.55.0", "eslint-import-resolver-typescript": "3.6.1", "eslint-plugin-import": "npm:@phryneas/eslint-plugin-import@2.27.5-pr.2813.2817.199971c", "eslint-plugin-local-rules": "2.0.1", @@ -172,7 +172,7 @@ "rxjs": "7.8.1", "size-limit": "11.0.0", "subscriptions-transport-ws": "0.11.0", - "terser": "5.24.0", + "terser": "5.26.0", "ts-api-utils": "1.0.3", "ts-jest": "29.1.1", "ts-jest-resolver": "2.0.1", From dabb1f88cfa9cd25ca55e1918a1c23202b0e108c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Dec 2023 10:47:07 -0500 Subject: [PATCH 57/90] chore(deps): update all dependencies - patch updates (#11421) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .circleci/config.yml | 2 +- package-lock.json | 179 +++++++++++++++++++++++-------------------- package.json | 22 +++--- 3 files changed, 108 insertions(+), 95 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 609041812f5..78dfff62626 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,7 +1,7 @@ version: 2.1 orbs: - secops: apollo/circleci-secops-orb@2.0.3 + secops: apollo/circleci-secops-orb@2.0.4 jobs: # Filesize: diff --git a/package-lock.json b/package-lock.json index c8a1b3868b5..a2a88bf002e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,15 +26,15 @@ "zen-observable-ts": "^1.2.5" }, "devDependencies": { - "@arethetypeswrong/cli": "0.13.2", + "@arethetypeswrong/cli": "0.13.3", "@babel/parser": "7.23.5", "@changesets/changelog-github": "0.5.0", "@changesets/cli": "2.27.1", "@graphql-tools/schema": "10.0.2", - "@microsoft/api-extractor": "7.38.3", + "@microsoft/api-extractor": "7.38.5", "@rollup/plugin-node-resolve": "11.2.1", - "@size-limit/esbuild-why": "11.0.0", - "@size-limit/preset-small-lib": "11.0.0", + "@size-limit/esbuild-why": "11.0.1", + "@size-limit/preset-small-lib": "11.0.1", "@testing-library/jest-dom": "6.1.5", "@testing-library/react": "14.1.2", "@testing-library/react-12": "npm:@testing-library/react@^12", @@ -44,11 +44,11 @@ "@types/fetch-mock": "7.3.8", "@types/glob": "8.1.0", "@types/hoist-non-react-statics": "3.3.5", - "@types/jest": "29.5.10", + "@types/jest": "29.5.11", "@types/lodash": "4.14.202", - "@types/node": "20.10.3", + "@types/node": "20.10.4", "@types/node-fetch": "2.6.9", - "@types/react": "18.2.41", + "@types/react": "18.2.43", "@types/react-dom": "18.2.17", "@types/relay-runtime": "14.1.14", "@types/use-sync-external-store": "0.0.6", @@ -76,7 +76,7 @@ "jest-junit": "16.0.0", "lodash": "4.17.21", "patch-package": "8.0.0", - "prettier": "3.1.0", + "prettier": "3.1.1", "react": "18.2.0", "react-17": "npm:react@^17", "react-dom": "18.2.0", @@ -89,16 +89,16 @@ "rollup-plugin-cleanup": "3.2.1", "rollup-plugin-terser": "7.0.2", "rxjs": "7.8.1", - "size-limit": "11.0.0", + "size-limit": "11.0.1", "subscriptions-transport-ws": "0.11.0", "terser": "5.26.0", "ts-api-utils": "1.0.3", "ts-jest": "29.1.1", "ts-jest-resolver": "2.0.1", "ts-morph": "20.0.0", - "ts-node": "10.9.1", + "ts-node": "10.9.2", "typedoc": "0.25.0", - "typescript": "5.3.2", + "typescript": "5.3.3", "wait-for-observables": "1.0.3", "web-streams-polyfill": "3.2.1", "whatwg-fetch": "3.6.19" @@ -150,12 +150,12 @@ "dev": true }, "node_modules/@arethetypeswrong/cli": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.13.2.tgz", - "integrity": "sha512-eqRWeFFiI58xwsiUfZSdZsmNCaqqtxmSPP9554ajiCDrB/aNzq5VktVK7dNiT9PamunNeoej4KbDBnkNwVacvg==", + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.13.3.tgz", + "integrity": "sha512-lA29j9fkRGq+hNE3zQGxD/d8WmjhimSaPU2887CBe0Yv3C1UbIWvy51mYerp3/NoevjBLKSWhHmP5oY/eavtjQ==", "dev": true, "dependencies": { - "@arethetypeswrong/core": "0.13.2", + "@arethetypeswrong/core": "0.13.3", "chalk": "^4.1.2", "cli-table3": "^0.6.3", "commander": "^10.0.1", @@ -207,9 +207,9 @@ } }, "node_modules/@arethetypeswrong/core": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.13.2.tgz", - "integrity": "sha512-1l6ygar+6TH4o1JipWWGCEZlOhAwEShm1yKx+CgIByNjCzufbu6k9DNbDmBjdouusNRhBIOYQe1UHnJig+GtAw==", + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.13.3.tgz", + "integrity": "sha512-oxa26D3z5DEv9LGzfJWV/6PhQd170dFfDOXl9J3cGRNLo2B68zj6/YOD1RJaYF/kmxechdXT1XyGUPOtkXv8Lg==", "dev": true, "dependencies": { "@andrewbranch/untar.js": "^1.0.3", @@ -237,6 +237,19 @@ "node": ">=10" } }, + "node_modules/@arethetypeswrong/core/node_modules/typescript": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", + "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@babel/code-frame": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.7.tgz", @@ -2456,15 +2469,15 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.38.3", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.38.3.tgz", - "integrity": "sha512-xt9iYyC5f39281j77JTA9C3ISJpW1XWkCcnw+2vM78CPnro6KhPfwQdPDfwS5JCPNuq0grm8cMdPUOPvrchDWw==", + "version": "7.38.5", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.38.5.tgz", + "integrity": "sha512-c/w2zfqBcBJxaCzpJNvFoouWewcYrUOfeu5ZkWCCIXTF9a/gXM85RGevEzlMAIEGM/kssAAZSXRJIZ3Q5vLFow==", "dev": true, "dependencies": { - "@microsoft/api-extractor-model": "7.28.2", + "@microsoft/api-extractor-model": "7.28.3", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.61.0", + "@rushstack/node-core-library": "3.62.0", "@rushstack/rig-package": "0.5.1", "@rushstack/ts-command-line": "4.17.1", "colors": "~1.2.1", @@ -2479,14 +2492,14 @@ } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.2.tgz", - "integrity": "sha512-vkojrM2fo3q4n4oPh4uUZdjJ2DxQ2+RnDQL/xhTWSRUNPF6P4QyrvY357HBxbnltKcYu+nNNolVqc6TIGQ73Ig==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.3.tgz", + "integrity": "sha512-wT/kB2oDbdZXITyDh2SQLzaWwTOFbV326fP0pUwNW00WeliARs0qjmXBWmGWardEzp2U3/axkO3Lboqun6vrig==", "dev": true, "dependencies": { "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.61.0" + "@rushstack/node-core-library": "3.62.0" } }, "node_modules/@microsoft/api-extractor/node_modules/semver": { @@ -2637,9 +2650,9 @@ "dev": true }, "node_modules/@rushstack/node-core-library": { - "version": "3.61.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.61.0.tgz", - "integrity": "sha512-tdOjdErme+/YOu4gPed3sFS72GhtWCgNV9oDsHDnoLY5oDfwjKUc9Z+JOZZ37uAxcm/OCahDHfuu2ugqrfWAVQ==", + "version": "3.62.0", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.62.0.tgz", + "integrity": "sha512-88aJn2h8UpSvdwuDXBv1/v1heM6GnBf3RjEy6ZPP7UnzHNCqOHA2Ut+ScYUbXcqIdfew9JlTAe3g+cnX9xQ/Aw==", "dev": true, "dependencies": { "colors": "~1.2.1", @@ -2745,25 +2758,25 @@ } }, "node_modules/@size-limit/esbuild": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@size-limit/esbuild/-/esbuild-11.0.0.tgz", - "integrity": "sha512-OOmba2ZuMpaUhmBXgCfgrO7L6zkUDwvFFfW8T+dK08968LQ79Q+kNgEXQAd+dhj9TlTkHyyEDczWmx16e9cXoQ==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@size-limit/esbuild/-/esbuild-11.0.1.tgz", + "integrity": "sha512-JXxzmDW7Rch6yxd4u8g6uE21g34oT7fk7Ex2gfDwN4TtciOghI3By4fqxXOwGYkDueEcIw3LXNGjHnTS8Dz5nA==", "dev": true, "dependencies": { - "esbuild": "^0.19.5", - "nanoid": "^5.0.3" + "esbuild": "^0.19.8", + "nanoid": "^5.0.4" }, "engines": { "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "size-limit": "11.0.0" + "size-limit": "11.0.1" } }, "node_modules/@size-limit/esbuild-why": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@size-limit/esbuild-why/-/esbuild-why-11.0.0.tgz", - "integrity": "sha512-YTEmxCBE5PF6LH9lvNYXJM/h7XqVjpCVFVf2NrsJ75d3HdU8q77WriI7/T1++reBf8GfUhY5RIyASdR1ZJ8S4w==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@size-limit/esbuild-why/-/esbuild-why-11.0.1.tgz", + "integrity": "sha512-yZm93gskbV+4/XBxQ3Aju+JxkmxaqmzAFm1h+fYk4RXvFb742dcACEXFDy2I8jummF7n7lV/UYNqVOFPLvHW0Q==", "dev": true, "dependencies": { "esbuild-visualizer": "^0.4.1", @@ -2773,7 +2786,7 @@ "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "size-limit": "11.0.0" + "size-limit": "11.0.1" } }, "node_modules/@size-limit/esbuild-why/node_modules/open": { @@ -2795,29 +2808,29 @@ } }, "node_modules/@size-limit/file": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@size-limit/file/-/file-11.0.0.tgz", - "integrity": "sha512-tTg6sSiFbiogiof3GV4iIRCPS4+46Hvq4QWXGXp00Be/tOnpglXF62xNpCfFwefx9YCXxCyeYSqqaRBjpRCsmQ==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@size-limit/file/-/file-11.0.1.tgz", + "integrity": "sha512-ioSYJ1WY66kc9+3dgTHi5mT/gcaNNCJ22xU87cjzfKiNxmol+lGsNKbplmrJf+QezvPH9kRIFOWxBjGY+DOt3g==", "dev": true, "engines": { "node": "^18.0.0 || >=20.0.0" }, "peerDependencies": { - "size-limit": "11.0.0" + "size-limit": "11.0.1" } }, "node_modules/@size-limit/preset-small-lib": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/@size-limit/preset-small-lib/-/preset-small-lib-11.0.0.tgz", - "integrity": "sha512-B4KDPbx5E8Vsn/aXilt2iAeofRBJdT8svQRSylTQPw5RkrumXUBKioM1dmWUXcnuHR2zUveJXlMxGmbdmxbJpQ==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/@size-limit/preset-small-lib/-/preset-small-lib-11.0.1.tgz", + "integrity": "sha512-c1N5/wN5FRQ03aOpoCw9ed2TP/1cmjt8vKAeTxO40OSfj6ImkpkMarl7e7pCnBElMULc993aUP5UjFhDN6bU4w==", "dev": true, "dependencies": { - "@size-limit/esbuild": "11.0.0", - "@size-limit/file": "11.0.0", - "size-limit": "11.0.0" + "@size-limit/esbuild": "11.0.1", + "@size-limit/file": "11.0.1", + "size-limit": "11.0.1" }, "peerDependencies": { - "size-limit": "11.0.0" + "size-limit": "11.0.1" } }, "node_modules/@testing-library/dom": { @@ -3230,9 +3243,9 @@ } }, "node_modules/@types/jest": { - "version": "29.5.10", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.10.tgz", - "integrity": "sha512-tE4yxKEphEyxj9s4inideLHktW/x6DwesIwWZ9NN1FKf9zbJYsnhBoA9vrHA/IuIOKwPa5PcFBNV4lpMIOEzyQ==", + "version": "29.5.11", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.11.tgz", + "integrity": "sha512-S2mHmYIVe13vrm6q4kN6fLYYAka15ALQki/vgDC3mIukEOx8WJlv0kQPM+d4w8Gp6u0uSdKND04IlTXBv0rwnQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -3313,9 +3326,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.10.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.3.tgz", - "integrity": "sha512-XJavIpZqiXID5Yxnxv3RUDKTN5b81ddNC3ecsA0SoFXz/QU8OGBwZGMomiq0zw+uuqbL/krztv/DINAQ/EV4gg==", + "version": "20.10.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.4.tgz", + "integrity": "sha512-D08YG6rr8X90YB56tSIuBaddy/UXAA9RKJoFvrsnogAum/0pmjkgi4+2nx96A330FmioegBWmEYQ+syqCFaveg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -3344,9 +3357,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.41", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.41.tgz", - "integrity": "sha512-CwOGr/PiLiNBxEBqpJ7fO3kocP/2SSuC9fpH5K7tusrg4xPSRT/193rzolYwQnTN02We/ATXKnb6GqA5w4fRxw==", + "version": "18.2.43", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.43.tgz", + "integrity": "sha512-nvOV01ZdBdd/KW6FahSbcNplt2jCJfyWdTos61RYHV+FVv5L/g9AOX1bmbVcWcLFL8+KHQfh1zVIQrud6ihyQA==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -8954,12 +8967,12 @@ } }, "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.0.0.tgz", + "integrity": "sha512-K2U4W2Ff5ibV7j7ydLr+zLAkIg5JJ4lPn1Ltsdt+Tz/IjQ8buJ55pZAxoP34lqIiwtF9iAvtLv3JGv7CAyAg+g==", "dev": true, "engines": { - "node": ">=10" + "node": ">=14" } }, "node_modules/lines-and-columns": { @@ -9410,9 +9423,9 @@ "dev": true }, "node_modules/nanoid": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.3.tgz", - "integrity": "sha512-I7X2b22cxA4LIHXPSqbBCEQSL+1wv8TuoefejsX4HFWyC6jc5JG7CEaxOltiKjc1M+YCS2YkrZZcj4+dytw9GA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.4.tgz", + "integrity": "sha512-vAjmBf13gsmhXSgBrtIclinISzFFy22WwCYoyilZlsrRXNIHSwgFQ1bEdjRwMT3aoadeIF6HMuDRlOxzfXV8ig==", "dev": true, "funding": [ { @@ -10190,9 +10203,9 @@ } }, "node_modules/prettier": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.0.tgz", - "integrity": "sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.1.1.tgz", + "integrity": "sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -11022,15 +11035,15 @@ "dev": true }, "node_modules/size-limit": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/size-limit/-/size-limit-11.0.0.tgz", - "integrity": "sha512-6+i4rE1GRzx/vRpuitRYQiZJNTXJjde+4P2NPg8AK7pURrE1+hA3mGstzvT8vQ8DuYFnvp9fh4CHM7Heq3EKXA==", + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/size-limit/-/size-limit-11.0.1.tgz", + "integrity": "sha512-6L80ocVspWPrhIRg8kPl41VypqTGH8/lu9e6TJiSJpkNLtOR2h/EEqdAO/wNJOv/sUVtjX+lVEWrzBpItGP+gQ==", "dev": true, "dependencies": { "bytes-iec": "^3.1.1", "chokidar": "^3.5.3", "globby": "^14.0.0", - "lilconfig": "^2.1.0", + "lilconfig": "^3.0.0", "nanospinner": "^1.1.0", "picocolors": "^1.0.0" }, @@ -11893,9 +11906,9 @@ } }, "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -11936,9 +11949,9 @@ } }, "node_modules/ts-node/node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.1.tgz", + "integrity": "sha512-TgUZgYvqZprrl7YldZNoa9OciCAyZR+Ejm9eXzKCmjsF5IKp/wgQ7Z/ZpjpGTIUPwrHQIcYeI8qDh4PsEwxMbw==", "dev": true, "engines": { "node": ">=0.4.0" @@ -12112,9 +12125,9 @@ } }, "node_modules/typescript": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz", - "integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index b98d0729d27..f99c1a6cdd9 100644 --- a/package.json +++ b/package.json @@ -107,15 +107,15 @@ "zen-observable-ts": "^1.2.5" }, "devDependencies": { - "@arethetypeswrong/cli": "0.13.2", + "@arethetypeswrong/cli": "0.13.3", "@babel/parser": "7.23.5", "@changesets/changelog-github": "0.5.0", "@changesets/cli": "2.27.1", "@graphql-tools/schema": "10.0.2", - "@microsoft/api-extractor": "7.38.3", + "@microsoft/api-extractor": "7.38.5", "@rollup/plugin-node-resolve": "11.2.1", - "@size-limit/esbuild-why": "11.0.0", - "@size-limit/preset-small-lib": "11.0.0", + "@size-limit/esbuild-why": "11.0.1", + "@size-limit/preset-small-lib": "11.0.1", "@testing-library/jest-dom": "6.1.5", "@testing-library/react": "14.1.2", "@testing-library/react-12": "npm:@testing-library/react@^12", @@ -125,11 +125,11 @@ "@types/fetch-mock": "7.3.8", "@types/glob": "8.1.0", "@types/hoist-non-react-statics": "3.3.5", - "@types/jest": "29.5.10", + "@types/jest": "29.5.11", "@types/lodash": "4.14.202", - "@types/node": "20.10.3", + "@types/node": "20.10.4", "@types/node-fetch": "2.6.9", - "@types/react": "18.2.41", + "@types/react": "18.2.43", "@types/react-dom": "18.2.17", "@types/relay-runtime": "14.1.14", "@types/use-sync-external-store": "0.0.6", @@ -157,7 +157,7 @@ "jest-junit": "16.0.0", "lodash": "4.17.21", "patch-package": "8.0.0", - "prettier": "3.1.0", + "prettier": "3.1.1", "react": "18.2.0", "react-17": "npm:react@^17", "react-dom": "18.2.0", @@ -170,16 +170,16 @@ "rollup-plugin-cleanup": "3.2.1", "rollup-plugin-terser": "7.0.2", "rxjs": "7.8.1", - "size-limit": "11.0.0", + "size-limit": "11.0.1", "subscriptions-transport-ws": "0.11.0", "terser": "5.26.0", "ts-api-utils": "1.0.3", "ts-jest": "29.1.1", "ts-jest-resolver": "2.0.1", "ts-morph": "20.0.0", - "ts-node": "10.9.1", + "ts-node": "10.9.2", "typedoc": "0.25.0", - "typescript": "5.3.2", + "typescript": "5.3.3", "wait-for-observables": "1.0.3", "web-streams-polyfill": "3.2.1", "whatwg-fetch": "3.6.19" From 2e7203b3a9618952ddb522627ded7cceabd7f250 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 15 Dec 2023 17:15:31 +0100 Subject: [PATCH 58/90] experimental `ApolloClient.getMemoryInternals` helper (#11409) * `print`: use `WeakCache` instead of `WeakMap` * format * pull in memory testing tools from PR 11358 * Persisted Query Link: improve memory management * re-add accidentally removed dependency * update api * update size limit * size-limit * fix test failure * better cleanup of interval/timeout * apply formatting * remove unneccessary type * format again after updating prettier * add central confiuguration for Apollo Client cache sizes * resolve import cycle * add exports * reduce cache collection throttle timeout * typo in comment * experimental `ApolloClient.getCacheStatus` helper * update size-limits * fix circular import * size-limits * update type to remove `WeakKey` * api-extractor * work around ES5 class compat * update api-report * fix typo in comment * chores * changeset * stop recursion * add some internal annotations and optional readonly cacheSize property * slight remodel, add more caches * chores * add more caches * add more caches * chores * add type export * update test * chores * formatting * adjust more tests * chores * rename, add more caches * rename file * chores * size-limits * Update wet-forks-rhyme.md * PR feedback, long explanatory comment * Clean up Prettier, Size-limit, and Api-Extractor * adjust comments * remove `expect.objectContaining` * flip conditionals * extract function * Clean up Prettier, Size-limit, and Api-Extractor * report correct size limits * Clean up Prettier, Size-limit, and Api-Extractor * adjust report shape to roughly match configuration * Clean up Prettier, Size-limit, and Api-Extractor * Update src/utilities/caching/getMemoryInternals.ts * Clean up Prettier, Size-limit, and Api-Extractor * better types * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: phryneas --- .api-reports/api-report-cache.md | 35 +++ .api-reports/api-report-core.md | 89 ++++++- .api-reports/api-report-link_batch-http.md | 6 + .api-reports/api-report-link_batch.md | 6 + .api-reports/api-report-link_context.md | 6 + .api-reports/api-report-link_core.md | 6 + .api-reports/api-report-link_error.md | 6 + .api-reports/api-report-link_http.md | 6 + .../api-report-link_persisted-queries.md | 16 +- .../api-report-link_remove-typename.md | 16 +- .api-reports/api-report-link_retry.md | 6 + .api-reports/api-report-link_schema.md | 6 + .api-reports/api-report-link_subscriptions.md | 6 + .api-reports/api-report-link_ws.md | 6 + .api-reports/api-report-react.md | 65 ++++- .api-reports/api-report-react_components.md | 65 ++++- .api-reports/api-report-react_context.md | 65 ++++- .api-reports/api-report-react_hoc.md | 65 ++++- .api-reports/api-report-react_hooks.md | 65 ++++- .api-reports/api-report-react_ssr.md | 65 ++++- .api-reports/api-report-testing.md | 65 ++++- .api-reports/api-report-testing_core.md | 65 ++++- .api-reports/api-report-utilities.md | 89 ++++++- .api-reports/api-report.md | 89 ++++++- .changeset/wet-forks-rhyme.md | 5 + .size-limits.json | 4 +- src/cache/core/cache.ts | 14 ++ src/cache/inmemory/inMemoryCache.ts | 14 ++ src/core/ApolloClient.ts | 14 ++ src/core/QueryInfo.ts | 5 +- src/link/core/ApolloLink.ts | 29 ++- src/link/persisted-queries/index.ts | 15 +- .../removeTypenameFromVariables.ts | 39 ++- src/react/parser/index.ts | 5 + .../caching/__tests__/getMemoryInternals.ts | 204 ++++++++++++++++ src/utilities/caching/getMemoryInternals.ts | 228 ++++++++++++++++++ src/utilities/common/canonicalStringify.ts | 5 + src/utilities/graphql/DocumentTransform.ts | 54 +++-- src/utilities/graphql/print.ts | 6 +- 39 files changed, 1502 insertions(+), 53 deletions(-) create mode 100644 .changeset/wet-forks-rhyme.md create mode 100644 src/utilities/caching/__tests__/getMemoryInternals.ts create mode 100644 src/utilities/caching/getMemoryInternals.ts diff --git a/.api-reports/api-report-cache.md b/.api-reports/api-report-cache.md index 1f2efd837fc..8cd3cb53be1 100644 --- a/.api-reports/api-report-cache.md +++ b/.api-reports/api-report-cache.md @@ -30,6 +30,10 @@ export abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -474,6 +478,33 @@ export interface FragmentRegistryAPI { transform(document: D): D; } +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getInMemoryCacheMemoryInternals: (() => { + addTypenameDocumentTransform: { + cache: number; + }[]; + inMemoryCache: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + }; + fragmentRegistry: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + }; + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + // @public (undocumented) export type IdGetter = (value: IdGetterObj) => string | undefined; @@ -513,6 +544,10 @@ export class InMemoryCache extends ApolloCache { resetResultCache?: boolean; resetResultIdentities?: boolean; }): string[]; + // Warning: (ae-forgotten-export) The symbol "getInMemoryCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index d2ec18f333b..16c8ea5ed87 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -46,6 +46,10 @@ export abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -104,6 +108,10 @@ export class ApolloClient implements DataProxy { disableNetworkFetches: boolean; get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; // (undocumented) @@ -221,10 +229,16 @@ export class ApolloLink { static execute(link: ApolloLink, operation: GraphQLRequest): Observable; // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // (undocumented) @@ -559,9 +573,16 @@ export class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -860,6 +881,68 @@ export function fromError(errorValue: any): Observable; // @public (undocumented) export function fromPromise(promise: Promise): Observable; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + +// @internal +const getInMemoryCacheMemoryInternals: (() => { + addTypenameDocumentTransform: { + cache: number; + }[]; + inMemoryCache: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + }; + fragmentRegistry: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + }; + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + export { gql } // @public (undocumented) @@ -973,6 +1056,10 @@ export class InMemoryCache extends ApolloCache { resetResultCache?: boolean; resetResultIdentities?: boolean; }): string[]; + // Warning: (ae-forgotten-export) The symbol "getInMemoryCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) diff --git a/.api-reports/api-report-link_batch-http.md b/.api-reports/api-report-link_batch-http.md index 4ea94f13b7b..569450e1771 100644 --- a/.api-reports/api-report-link_batch-http.md +++ b/.api-reports/api-report-link_batch-http.md @@ -29,12 +29,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_batch.md b/.api-reports/api-report-link_batch.md index 7562ad80181..a547973287d 100644 --- a/.api-reports/api-report-link_batch.md +++ b/.api-reports/api-report-link_batch.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_context.md b/.api-reports/api-report-link_context.md index bd2790f5381..af79db73e52 100644 --- a/.api-reports/api-report-link_context.md +++ b/.api-reports/api-report-link_context.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_core.md b/.api-reports/api-report-link_core.md index e711dfe7fc1..f488d284b51 100644 --- a/.api-reports/api-report-link_core.md +++ b/.api-reports/api-report-link_core.md @@ -23,10 +23,16 @@ export class ApolloLink { static execute(link: ApolloLink, operation: GraphQLRequest): Observable; // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // (undocumented) diff --git a/.api-reports/api-report-link_error.md b/.api-reports/api-report-link_error.md index febf5b6be00..af048d6fe6b 100644 --- a/.api-reports/api-report-link_error.md +++ b/.api-reports/api-report-link_error.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_http.md b/.api-reports/api-report-link_http.md index 3849a3649df..30f6fa9210c 100644 --- a/.api-reports/api-report-link_http.md +++ b/.api-reports/api-report-link_http.md @@ -30,12 +30,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_persisted-queries.md b/.api-reports/api-report-link_persisted-queries.md index 353d48364e6..14e7a0b47db 100644 --- a/.api-reports/api-report-link_persisted-queries.md +++ b/.api-reports/api-report-link_persisted-queries.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -59,7 +65,15 @@ interface BaseOptions { // @public (undocumented) export const createPersistedQueryLink: (options: PersistedQueryLink.Options) => ApolloLink & { resetHashCache: () => void; -}; +} & ({ + getMemoryInternals(): { + PersistedQueryLink: { + persistedQueryHashes: number; + }; + }; +} | { + getMemoryInternals?: undefined; +}); // @public (undocumented) interface DefaultContext extends Record { diff --git a/.api-reports/api-report-link_remove-typename.md b/.api-reports/api-report-link_remove-typename.md index ddec205b0ac..f50798f5f02 100644 --- a/.api-reports/api-report-link_remove-typename.md +++ b/.api-reports/api-report-link_remove-typename.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -160,7 +166,15 @@ type Path = ReadonlyArray; // Warning: (ae-forgotten-export) The symbol "ApolloLink" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function removeTypenameFromVariables(options?: RemoveTypenameFromVariablesOptions): ApolloLink; +export function removeTypenameFromVariables(options?: RemoveTypenameFromVariablesOptions): ApolloLink & ({ + getMemoryInternals(): { + removeTypenameFromVariables: { + getVariableDefinitions: number; + }; + }; +} | { + getMemoryInternals?: undefined; +}); // @public (undocumented) export interface RemoveTypenameFromVariablesOptions { diff --git a/.api-reports/api-report-link_retry.md b/.api-reports/api-report-link_retry.md index 1c3d0f2557d..a4a61a6ea1d 100644 --- a/.api-reports/api-report-link_retry.md +++ b/.api-reports/api-report-link_retry.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_schema.md b/.api-reports/api-report-link_schema.md index 31712ec362d..fcbee50828b 100644 --- a/.api-reports/api-report-link_schema.md +++ b/.api-reports/api-report-link_schema.md @@ -29,12 +29,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_subscriptions.md b/.api-reports/api-report-link_subscriptions.md index 5b5e1585f6e..8745a5772cb 100644 --- a/.api-reports/api-report-link_subscriptions.md +++ b/.api-reports/api-report-link_subscriptions.md @@ -29,12 +29,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_ws.md b/.api-reports/api-report-link_ws.md index 4ee65142d54..72a8165e4f0 100644 --- a/.api-reports/api-report-link_ws.md +++ b/.api-reports/api-report-link_ws.md @@ -30,12 +30,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 7a8606ad8fe..4b1402c6440 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -41,6 +41,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -115,6 +119,10 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -281,12 +289,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -678,9 +692,16 @@ class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -815,6 +836,48 @@ interface FulfilledPromise extends Promise { value: TValue; } +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) export function getApolloContext(): ReactTypes.Context; diff --git a/.api-reports/api-report-react_components.md b/.api-reports/api-report-react_components.md index b009330d47e..96429688c38 100644 --- a/.api-reports/api-report-react_components.md +++ b/.api-reports/api-report-react_components.md @@ -41,6 +41,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -115,6 +119,10 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -259,12 +267,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -592,9 +606,16 @@ class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -697,6 +718,48 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) type GraphQLErrors = ReadonlyArray; diff --git a/.api-reports/api-report-react_context.md b/.api-reports/api-report-react_context.md index ead8fac4345..48fee485c29 100644 --- a/.api-reports/api-report-react_context.md +++ b/.api-reports/api-report-react_context.md @@ -40,6 +40,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -114,6 +118,10 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -279,12 +287,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -575,9 +589,16 @@ class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -680,6 +701,48 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) export function getApolloContext(): ReactTypes.Context; diff --git a/.api-reports/api-report-react_hoc.md b/.api-reports/api-report-react_hoc.md index 7fde0ee923b..1a4c9bbedc9 100644 --- a/.api-reports/api-report-react_hoc.md +++ b/.api-reports/api-report-react_hoc.md @@ -40,6 +40,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -114,6 +118,10 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -258,12 +266,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -577,9 +591,16 @@ class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -691,6 +712,48 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) export function graphql> & Partial>>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index 02ae3dbf3b9..457f93bddd0 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -39,6 +39,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -113,6 +117,10 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -257,12 +265,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -649,9 +663,16 @@ class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -775,6 +796,48 @@ interface FulfilledPromise extends Promise { value: TValue; } +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) type GraphQLErrors = ReadonlyArray; diff --git a/.api-reports/api-report-react_ssr.md b/.api-reports/api-report-react_ssr.md index 923a6b2f00a..74218bc5e08 100644 --- a/.api-reports/api-report-react_ssr.md +++ b/.api-reports/api-report-react_ssr.md @@ -40,6 +40,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -114,6 +118,10 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -258,12 +266,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -545,9 +559,16 @@ class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -650,6 +671,48 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) export function getDataFromTree(tree: ReactTypes.ReactNode, context?: { [key: string]: any; diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index 87e34a9e2b6..2a69d2c4e80 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -40,6 +40,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -114,6 +118,10 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -258,12 +266,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -539,9 +553,16 @@ class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -644,6 +665,48 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) type GraphQLErrors = ReadonlyArray; diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index ca6f6c60b4e..dd02053d327 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -39,6 +39,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -113,6 +117,10 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -257,12 +265,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -538,9 +552,16 @@ class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -643,6 +664,48 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) type GraphQLErrors = ReadonlyArray; diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 65911cc6b02..6a64515659b 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -57,6 +57,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -126,6 +130,10 @@ class ApolloClient implements DataProxy { disableNetworkFetches: boolean; get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -270,12 +278,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts // // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -783,9 +797,16 @@ export class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -1068,6 +1089,48 @@ interface FulfilledPromise extends Promise { value: TValue; } +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) export function getDefaultValues(definition: OperationDefinitionNode | undefined): Record; @@ -1098,6 +1161,26 @@ export function getGraphQLErrorsFromResult(result: FetchResult): GraphQLEr // @public (undocumented) export function getInclusionDirectives(directives: ReadonlyArray): InclusionDirectives; +// @internal +const getInMemoryCacheMemoryInternals: (() => { + addTypenameDocumentTransform: { + cache: number; + }[]; + inMemoryCache: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + }; + fragmentRegistry: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + }; + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + // @public export function getMainDefinition(queryDoc: DocumentNode): OperationDefinitionNode | FragmentDefinitionNode; @@ -1219,6 +1302,10 @@ class InMemoryCache extends ApolloCache { resetResultCache?: boolean; resetResultIdentities?: boolean; }): string[]; + // Warning: (ae-forgotten-export) The symbol "getInMemoryCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // Warning: (ae-forgotten-export) The symbol "makeVar" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index d3549849065..ac97e197077 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -48,6 +48,10 @@ export abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -106,6 +110,10 @@ export class ApolloClient implements DataProxy { disableNetworkFetches: boolean; get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; // (undocumented) @@ -244,10 +252,16 @@ export class ApolloLink { static execute(link: ApolloLink, operation: GraphQLRequest): Observable; // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // (undocumented) @@ -702,9 +716,16 @@ export class DocumentTransform { concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; resetCache(): void; + // @internal + readonly right?: DocumentTransform; // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -1033,9 +1054,71 @@ interface FulfilledPromise extends Promise { value: TValue; } +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) export function getApolloContext(): ReactTypes.Context; +// @internal +const getInMemoryCacheMemoryInternals: (() => { + addTypenameDocumentTransform: { + cache: number; + }[]; + inMemoryCache: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + }; + fragmentRegistry: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + }; + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + export { gql } // @public (undocumented) @@ -1159,6 +1242,10 @@ export class InMemoryCache extends ApolloCache { resetResultCache?: boolean; resetResultIdentities?: boolean; }): string[]; + // Warning: (ae-forgotten-export) The symbol "getInMemoryCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) diff --git a/.changeset/wet-forks-rhyme.md b/.changeset/wet-forks-rhyme.md new file mode 100644 index 00000000000..2fc57066943 --- /dev/null +++ b/.changeset/wet-forks-rhyme.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Adds an experimental `ApolloClient.getMemoryInternals` helper diff --git a/.size-limits.json b/.size-limits.json index f6a846be06c..cffdc4320a2 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38831, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32603 + "dist/apollo-client.min.cjs": 38813, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32610 } diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index b90871cdd82..a0fd1778bdc 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -10,6 +10,7 @@ import { import type { DataProxy } from "./types/DataProxy.js"; import type { Cache } from "./types/Cache.js"; import { WeakCache } from "@wry/caches"; +import { getApolloCacheMemoryInternals } from "../../utilities/caching/getMemoryInternals.js"; export type Transaction = (c: ApolloCache) => void; @@ -219,4 +220,17 @@ export abstract class ApolloCache implements DataProxy { }, }); } + + /** + * @experimental + * @internal + * This is not a stable API - it is used in development builds to expose + * information to the DevTools. + * Use at your own risk! + */ + public getMemoryInternals?: typeof getApolloCacheMemoryInternals; +} + +if (__DEV__) { + ApolloCache.prototype.getMemoryInternals = getApolloCacheMemoryInternals; } diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index 6e001cd9137..fe62023f165 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -29,6 +29,7 @@ import { makeVar, forgetCache, recallCache } from "./reactiveVars.js"; import { Policies } from "./policies.js"; import { hasOwn, normalizeConfig, shouldCanonizeResults } from "./helpers.js"; import type { OperationVariables } from "../../core/index.js"; +import { getInMemoryCacheMemoryInternals } from "../../utilities/caching/getMemoryInternals.js"; type BroadcastOptions = Pick< Cache.BatchOptions, @@ -581,4 +582,17 @@ export class InMemoryCache extends ApolloCache { c.callback((c.lastDiff = diff), lastDiff); } } + + /** + * @experimental + * @internal + * This is not a stable API - it is used in development builds to expose + * information to the DevTools. + * Use at your own risk! + */ + public getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; +} + +if (__DEV__) { + InMemoryCache.prototype.getMemoryInternals = getInMemoryCacheMemoryInternals; } diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 050245e9d8b..a3216fdda34 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -122,6 +122,7 @@ export interface ApolloClientOptions { // @apollo/client/core. Since we need to preserve that API anyway, the easiest // solution is to reexport mergeOptions where it was previously declared (here). import { mergeOptions } from "../utilities/index.js"; +import { getApolloClientMemoryInternals } from "../utilities/caching/getMemoryInternals.js"; export { mergeOptions }; /** @@ -746,4 +747,17 @@ export class ApolloClient implements DataProxy { public get defaultContext() { return this.queryManager.defaultContext; } + + /** + * @experimental + * @internal + * This is not a stable API - it is used in development builds to expose + * information to the DevTools. + * Use at your own risk! + */ + public getMemoryInternals?: typeof getApolloClientMemoryInternals; +} + +if (__DEV__) { + ApolloClient.prototype.getMemoryInternals = getApolloClientMemoryInternals; } diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index 02e1768af17..b3dd4366cf2 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -7,7 +7,7 @@ import { mergeIncrementalData } from "../utilities/index.js"; import type { WatchQueryOptions, ErrorPolicy } from "./watchQueryOptions.js"; import type { ObservableQuery } from "./ObservableQuery.js"; import { reobserveCacheFirst } from "./ObservableQuery.js"; -import type { QueryListener, MethodKeys } from "./types.js"; +import type { QueryListener } from "./types.js"; import type { FetchResult } from "../link/core/index.js"; import { isNonEmptyArray, @@ -36,10 +36,11 @@ const destructiveMethodCounts = new (canUseWeakMap ? WeakMap : Map)< function wrapDestructiveCacheMethod( cache: ApolloCache, - methodName: MethodKeys> + methodName: "evict" | "modify" | "reset" ) { const original = cache[methodName]; if (typeof original === "function") { + // @ts-expect-error this is just too generic to be typed correctly cache[methodName] = function () { destructiveMethodCounts.set( cache, diff --git a/src/link/core/ApolloLink.ts b/src/link/core/ApolloLink.ts index ca9d2cfcd72..4f7759915d6 100644 --- a/src/link/core/ApolloLink.ts +++ b/src/link/core/ApolloLink.ts @@ -45,19 +45,21 @@ export class ApolloLink { const leftLink = toLink(left); const rightLink = toLink(right || new ApolloLink(passthrough)); + let ret: ApolloLink; if (isTerminating(leftLink) && isTerminating(rightLink)) { - return new ApolloLink((operation) => { + ret = new ApolloLink((operation) => { return test(operation) ? leftLink.request(operation) || Observable.of() : rightLink.request(operation) || Observable.of(); }); } else { - return new ApolloLink((operation, forward) => { + ret = new ApolloLink((operation, forward) => { return test(operation) ? leftLink.request(operation, forward) || Observable.of() : rightLink.request(operation, forward) || Observable.of(); }); } + return Object.assign(ret, { left: leftLink, right: rightLink }); } public static execute( @@ -88,8 +90,9 @@ export class ApolloLink { } const nextLink = toLink(second); + let ret: ApolloLink; if (isTerminating(nextLink)) { - return new ApolloLink( + ret = new ApolloLink( (operation) => firstLink.request( operation, @@ -97,7 +100,7 @@ export class ApolloLink { ) || Observable.of() ); } else { - return new ApolloLink((operation, forward) => { + ret = new ApolloLink((operation, forward) => { return ( firstLink.request(operation, (op) => { return nextLink.request(op, forward) || Observable.of(); @@ -105,6 +108,7 @@ export class ApolloLink { ); }); } + return Object.assign(ret, { left: firstLink, right: nextLink }); } constructor(request?: RequestHandler) { @@ -154,4 +158,21 @@ export class ApolloLink { this.onError = fn; return this; } + + /** + * @internal + * Used to iterate through all links that are concatenations or `split` links. + */ + readonly left?: ApolloLink; + /** + * @internal + * Used to iterate through all links that are concatenations or `split` links. + */ + readonly right?: ApolloLink; + + /** + * @internal + * Can be provided by a link that has an internal cache to report it's memory details. + */ + getMemoryInternals?: () => unknown; } diff --git a/src/link/persisted-queries/index.ts b/src/link/persisted-queries/index.ts index 400af11d6f5..920633bd7f3 100644 --- a/src/link/persisted-queries/index.ts +++ b/src/link/persisted-queries/index.ts @@ -301,6 +301,19 @@ export const createPersistedQueryLink = ( }; }); }), - { resetHashCache } + { + resetHashCache, + }, + __DEV__ ? + { + getMemoryInternals() { + return { + PersistedQueryLink: { + persistedQueryHashes: hashesByQuery?.size ?? 0, + }, + }; + }, + } + : {} ); }; diff --git a/src/link/remove-typename/removeTypenameFromVariables.ts b/src/link/remove-typename/removeTypenameFromVariables.ts index b713a5b4138..31ed9c58a9c 100644 --- a/src/link/remove-typename/removeTypenameFromVariables.ts +++ b/src/link/remove-typename/removeTypenameFromVariables.ts @@ -24,19 +24,32 @@ export interface RemoveTypenameFromVariablesOptions { export function removeTypenameFromVariables( options: RemoveTypenameFromVariablesOptions = Object.create(null) ) { - return new ApolloLink((operation, forward) => { - const { except } = options; - const { query, variables } = operation; - - if (variables) { - operation.variables = - except ? - maybeStripTypenameUsingConfig(query, variables, except) - : stripTypename(variables); - } - - return forward(operation); - }); + return Object.assign( + new ApolloLink((operation, forward) => { + const { except } = options; + const { query, variables } = operation; + + if (variables) { + operation.variables = + except ? + maybeStripTypenameUsingConfig(query, variables, except) + : stripTypename(variables); + } + + return forward(operation); + }), + __DEV__ ? + { + getMemoryInternals() { + return { + removeTypenameFromVariables: { + getVariableDefinitions: getVariableDefinitions?.size ?? 0, + }, + }; + }, + } + : {} + ); } function maybeStripTypenameUsingConfig( diff --git a/src/react/parser/index.ts b/src/react/parser/index.ts index 5cca2c066f8..6bcf2989989 100644 --- a/src/react/parser/index.ts +++ b/src/react/parser/index.ts @@ -11,6 +11,7 @@ import { cacheSizes, defaultCacheSizes, } from "../../utilities/index.js"; +import { registerGlobalCache } from "../../utilities/caching/getMemoryInternals.js"; export enum DocumentType { Query, @@ -153,6 +154,10 @@ parser.resetCache = () => { cache = undefined; }; +if (__DEV__) { + registerGlobalCache("parser", () => (cache ? cache.size : 0)); +} + export function verifyDocumentType(document: DocumentNode, type: DocumentType) { const operation = parser(document); const requiredOperationName = operationName(type); diff --git a/src/utilities/caching/__tests__/getMemoryInternals.ts b/src/utilities/caching/__tests__/getMemoryInternals.ts new file mode 100644 index 00000000000..c83f34da27f --- /dev/null +++ b/src/utilities/caching/__tests__/getMemoryInternals.ts @@ -0,0 +1,204 @@ +import { createFragmentRegistry } from "../../../cache"; +import { + ApolloClient, + ApolloLink, + DocumentTransform, + InMemoryCache, + gql, +} from "../../../core"; +import { createPersistedQueryLink } from "../../../link/persisted-queries"; +import { removeTypenameFromVariables } from "../../../link/remove-typename"; +import crypto from "crypto"; +// importing react so the `parser` cache initializes +import "../../../react"; +import { cacheSizes, defaultCacheSizes } from "../sizes"; + +function sha256(data: string) { + const hash = crypto.createHash("sha256"); + hash.update(data); + return hash.digest("hex"); +} + +const defaultCacheSizesAsObject = { + parser: defaultCacheSizes["parser"], + canonicalStringify: defaultCacheSizes["canonicalStringify"], + print: defaultCacheSizes["print"], + "documentTransform.cache": defaultCacheSizes["documentTransform.cache"], + "queryManager.getDocumentInfo": + defaultCacheSizes["queryManager.getDocumentInfo"], + "PersistedQueryLink.persistedQueryHashes": + defaultCacheSizes["PersistedQueryLink.persistedQueryHashes"], + "fragmentRegistry.transform": defaultCacheSizes["fragmentRegistry.transform"], + "fragmentRegistry.lookup": defaultCacheSizes["fragmentRegistry.lookup"], + "fragmentRegistry.findFragmentSpreads": + defaultCacheSizes["fragmentRegistry.findFragmentSpreads"], + "cache.fragmentQueryDocuments": + defaultCacheSizes["cache.fragmentQueryDocuments"], + "removeTypenameFromVariables.getVariableDefinitions": + defaultCacheSizes["removeTypenameFromVariables.getVariableDefinitions"], + "inMemoryCache.maybeBroadcastWatch": + defaultCacheSizes["inMemoryCache.maybeBroadcastWatch"], + "inMemoryCache.executeSelectionSet": + defaultCacheSizes["inMemoryCache.executeSelectionSet"], + "inMemoryCache.executeSubSelectedArray": + defaultCacheSizes["inMemoryCache.executeSubSelectedArray"], +}; + +it("returns information about cache usage (empty caches)", () => { + const client = new ApolloClient({ + documentTransform: new DocumentTransform((x) => x, { + cache: true, + }).concat( + new DocumentTransform((x) => x, { + cache: true, + }) + ), + cache: new InMemoryCache({ + fragments: createFragmentRegistry(), + }), + link: createPersistedQueryLink({ + sha256, + }) + .concat(removeTypenameFromVariables()) + .concat(ApolloLink.empty()), + }); + expect(client.getMemoryInternals?.()).toEqual({ + limits: defaultCacheSizesAsObject, + sizes: { + parser: 0, + canonicalStringify: 0, + print: 0, + addTypenameDocumentTransform: [ + { + cache: 0, + }, + ], + queryManager: { + getDocumentInfo: 0, + documentTransforms: [ + { + cache: 0, + }, + { + cache: 0, + }, + ], + }, + fragmentRegistry: { + findFragmentSpreads: 0, + lookup: 0, + transform: 0, + }, + cache: { + fragmentQueryDocuments: 0, + }, + inMemoryCache: { + executeSelectionSet: 0, + executeSubSelectedArray: 0, + maybeBroadcastWatch: 0, + }, + links: [ + { + PersistedQueryLink: { + persistedQueryHashes: 0, + }, + }, + { + removeTypenameFromVariables: { + getVariableDefinitions: 0, + }, + }, + ], + }, + }); +}); + +it("returns information about cache usage (some query triggered)", () => { + const client = new ApolloClient({ + documentTransform: new DocumentTransform((x) => x, { + cache: true, + }).concat( + new DocumentTransform((x) => x, { + cache: true, + }) + ), + cache: new InMemoryCache({ + fragments: createFragmentRegistry(), + }), + link: createPersistedQueryLink({ + sha256, + }) + .concat(removeTypenameFromVariables()) + .concat(ApolloLink.empty()), + }); + + client.query({ + query: gql` + query { + hello + } + `, + }); + expect(client.getMemoryInternals?.()).toStrictEqual({ + limits: defaultCacheSizesAsObject, + sizes: { + parser: 0, + canonicalStringify: 0, + print: 1, + addTypenameDocumentTransform: [ + { + cache: 1, + }, + ], + queryManager: { + getDocumentInfo: 1, + documentTransforms: [ + { + cache: 1, + }, + { + cache: 1, + }, + ], + }, + fragmentRegistry: { + findFragmentSpreads: 1, + lookup: 0, + transform: 1, + }, + cache: { + fragmentQueryDocuments: 0, + }, + inMemoryCache: { + executeSelectionSet: 1, + executeSubSelectedArray: 0, + maybeBroadcastWatch: 0, + }, + links: [ + { + PersistedQueryLink: { + persistedQueryHashes: 1, + }, + }, + { + removeTypenameFromVariables: { + getVariableDefinitions: 0, + }, + }, + ], + }, + }); +}); + +it("reports user-declared cacheSizes", () => { + const client = new ApolloClient({ + cache: new InMemoryCache({}), + }); + + cacheSizes["inMemoryCache.executeSubSelectedArray"] = 90; + + expect(client.getMemoryInternals?.().limits).toStrictEqual({ + ...defaultCacheSizesAsObject, + "inMemoryCache.executeSubSelectedArray": 90, + }); +}); diff --git a/src/utilities/caching/getMemoryInternals.ts b/src/utilities/caching/getMemoryInternals.ts new file mode 100644 index 00000000000..ac28989c37b --- /dev/null +++ b/src/utilities/caching/getMemoryInternals.ts @@ -0,0 +1,228 @@ +import type { OptimisticWrapperFunction } from "optimism"; +import type { + InMemoryCache, + DocumentTransform, + ApolloLink, + ApolloCache, +} from "../../core/index.js"; +import type { ApolloClient } from "../../core/index.js"; +import type { CacheSizes } from "./sizes.js"; +import { cacheSizes, defaultCacheSizes } from "./sizes.js"; + +const globalCaches: { + print?: () => number; + parser?: () => number; + canonicalStringify?: () => number; +} = {}; + +export function registerGlobalCache( + name: keyof typeof globalCaches, + getSize: () => number +) { + globalCaches[name] = getSize; +} + +/** + * Transformative helper type to turn a function of the form + * ```ts + * (this: any) => R + * ``` + * into a function of the form + * ```ts + * () => R + * ``` + * preserving the return type, but removing the `this` parameter. + * + * @remarks + * + * Further down in the definitions of `_getApolloClientMemoryInternals`, + * `_getApolloCacheMemoryInternals` and `_getInMemoryCacheMemoryInternals`, + * having the `this` parameter annotation is extremely useful for type checking + * inside the function. + * + * If this is preserved in the exported types, though, it leads to a situation + * where `ApolloCache.getMemoryInternals` is a function that requires a `this` + * of the type `ApolloCache`, while the extending class `InMemoryCache` has a + * `getMemoryInternals` function that requires a `this` of the type + * `InMemoryCache`. + * This is not compatible with TypeScript's inheritence system (although it is + * perfectly correct), and so TypeScript will complain loudly. + * + * We still want to define our functions with the `this` annotation, though, + * and have the return type inferred. + * (This requirement for return type inference here makes it impossible to use + * a function overload that is more explicit on the inner overload than it is + * on the external overload.) + * + * So in the end, we use this helper to remove the `this` annotation from the + * exported function types, while keeping it in the internal implementation. + * + */ +type RemoveThis = T extends (this: any) => infer R ? () => R : never; + +/** + * For internal purposes only - please call `ApolloClient.getMemoryInternals` instead + * @internal + */ +export const getApolloClientMemoryInternals = + __DEV__ ? + (_getApolloClientMemoryInternals as RemoveThis< + typeof _getApolloClientMemoryInternals + >) + : undefined; + +/** + * For internal purposes only - please call `ApolloClient.getMemoryInternals` instead + * @internal + */ +export const getInMemoryCacheMemoryInternals = + __DEV__ ? + (_getInMemoryCacheMemoryInternals as RemoveThis< + typeof _getInMemoryCacheMemoryInternals + >) + : undefined; + +/** + * For internal purposes only - please call `ApolloClient.getMemoryInternals` instead + * @internal + */ +export const getApolloCacheMemoryInternals = + __DEV__ ? + (_getApolloCacheMemoryInternals as RemoveThis< + typeof _getApolloCacheMemoryInternals + >) + : undefined; + +function getCurrentCacheSizes() { + // `defaultCacheSizes` is a `const enum` that will be inlined during build, so we have to reconstruct it's shape here + const defaults: Record = { + parser: defaultCacheSizes["parser"], + canonicalStringify: defaultCacheSizes["canonicalStringify"], + print: defaultCacheSizes["print"], + "documentTransform.cache": defaultCacheSizes["documentTransform.cache"], + "queryManager.getDocumentInfo": + defaultCacheSizes["queryManager.getDocumentInfo"], + "PersistedQueryLink.persistedQueryHashes": + defaultCacheSizes["PersistedQueryLink.persistedQueryHashes"], + "fragmentRegistry.transform": + defaultCacheSizes["fragmentRegistry.transform"], + "fragmentRegistry.lookup": defaultCacheSizes["fragmentRegistry.lookup"], + "fragmentRegistry.findFragmentSpreads": + defaultCacheSizes["fragmentRegistry.findFragmentSpreads"], + "cache.fragmentQueryDocuments": + defaultCacheSizes["cache.fragmentQueryDocuments"], + "removeTypenameFromVariables.getVariableDefinitions": + defaultCacheSizes["removeTypenameFromVariables.getVariableDefinitions"], + "inMemoryCache.maybeBroadcastWatch": + defaultCacheSizes["inMemoryCache.maybeBroadcastWatch"], + "inMemoryCache.executeSelectionSet": + defaultCacheSizes["inMemoryCache.executeSelectionSet"], + "inMemoryCache.executeSubSelectedArray": + defaultCacheSizes["inMemoryCache.executeSubSelectedArray"], + }; + return Object.fromEntries( + Object.entries(defaults).map(([k, v]) => [ + k, + cacheSizes[k as keyof CacheSizes] || v, + ]) + ); +} + +function _getApolloClientMemoryInternals(this: ApolloClient) { + if (!__DEV__) throw new Error("only supported in development mode"); + + return { + limits: getCurrentCacheSizes(), + sizes: { + print: globalCaches.print?.(), + parser: globalCaches.parser?.(), + canonicalStringify: globalCaches.canonicalStringify?.(), + links: linkInfo(this.link), + queryManager: { + getDocumentInfo: this["queryManager"]["transformCache"].size, + documentTransforms: transformInfo( + this["queryManager"].documentTransform + ), + }, + ...(this.cache.getMemoryInternals?.() as Partial< + ReturnType + > & + Partial>), + }, + }; +} + +function _getApolloCacheMemoryInternals(this: ApolloCache) { + return { + cache: { + fragmentQueryDocuments: getWrapperInformation(this["getFragmentDoc"]), + }, + }; +} + +function _getInMemoryCacheMemoryInternals(this: InMemoryCache) { + const fragments = this.config.fragments as + | undefined + | { + findFragmentSpreads?: Function; + transform?: Function; + lookup?: Function; + }; + + return { + ..._getApolloCacheMemoryInternals.apply(this as any), + addTypenameDocumentTransform: transformInfo(this["addTypenameTransform"]), + inMemoryCache: { + executeSelectionSet: getWrapperInformation( + this["storeReader"]["executeSelectionSet"] + ), + executeSubSelectedArray: getWrapperInformation( + this["storeReader"]["executeSubSelectedArray"] + ), + maybeBroadcastWatch: getWrapperInformation(this["maybeBroadcastWatch"]), + }, + fragmentRegistry: { + findFragmentSpreads: getWrapperInformation( + fragments?.findFragmentSpreads + ), + lookup: getWrapperInformation(fragments?.lookup), + transform: getWrapperInformation(fragments?.transform), + }, + }; +} + +function isWrapper(f?: Function): f is OptimisticWrapperFunction { + return !!f && "dirtyKey" in f; +} + +function getWrapperInformation(f?: Function) { + return isWrapper(f) ? f.size : undefined; +} + +function isDefined(value: T | undefined | null): value is T { + return value != null; +} + +function transformInfo(transform?: DocumentTransform) { + return recurseTransformInfo(transform).map((cache) => ({ cache })); +} + +function recurseTransformInfo(transform?: DocumentTransform): number[] { + return transform ? + [ + getWrapperInformation(transform?.["performWork"]), + ...recurseTransformInfo(transform?.["left"]), + ...recurseTransformInfo(transform?.["right"]), + ].filter(isDefined) + : []; +} + +function linkInfo(link?: ApolloLink): unknown[] { + return link ? + [ + link?.getMemoryInternals?.(), + ...linkInfo(link?.left), + ...linkInfo(link?.right), + ].filter(isDefined) + : []; +} diff --git a/src/utilities/common/canonicalStringify.ts b/src/utilities/common/canonicalStringify.ts index 7c037b0a680..adcc9898211 100644 --- a/src/utilities/common/canonicalStringify.ts +++ b/src/utilities/common/canonicalStringify.ts @@ -3,6 +3,7 @@ import { cacheSizes, defaultCacheSizes, } from "../../utilities/caching/index.js"; +import { registerGlobalCache } from "../caching/getMemoryInternals.js"; /** * Like JSON.stringify, but with object keys always sorted in the same order. @@ -37,6 +38,10 @@ export const canonicalStringify = Object.assign( } ); +if (__DEV__) { + registerGlobalCache("canonicalStringify", () => sortingMap.size); +} + // Values are JSON-serialized arrays of object keys (in any order), and values // are sorted arrays of the same keys. let sortingMap!: AutoCleanedStrongCache; diff --git a/src/utilities/graphql/DocumentTransform.ts b/src/utilities/graphql/DocumentTransform.ts index bf016aee0da..732c1c7d1a6 100644 --- a/src/utilities/graphql/DocumentTransform.ts +++ b/src/utilities/graphql/DocumentTransform.ts @@ -52,14 +52,17 @@ export class DocumentTransform { left: DocumentTransform, right: DocumentTransform = DocumentTransform.identity() ) { - return new DocumentTransform( - (document) => { - const documentTransform = predicate(document) ? left : right; - - return documentTransform.transformDocument(document); - }, - // Reasonably assume both `left` and `right` transforms handle their own caching - { cache: false } + return Object.assign( + new DocumentTransform( + (document) => { + const documentTransform = predicate(document) ? left : right; + + return documentTransform.transformDocument(document); + }, + // Reasonably assume both `left` and `right` transforms handle their own caching + { cache: false } + ), + { left, right } ); } @@ -123,15 +126,32 @@ export class DocumentTransform { return transformedDocument; } - concat(otherTransform: DocumentTransform) { - return new DocumentTransform( - (document) => { - return otherTransform.transformDocument( - this.transformDocument(document) - ); - }, - // Reasonably assume both transforms handle their own caching - { cache: false } + concat(otherTransform: DocumentTransform): DocumentTransform { + return Object.assign( + new DocumentTransform( + (document) => { + return otherTransform.transformDocument( + this.transformDocument(document) + ); + }, + // Reasonably assume both transforms handle their own caching + { cache: false } + ), + { + left: this, + right: otherTransform, + } ); } + + /** + * @internal + * Used to iterate through all transforms that are concatenations or `split` links. + */ + readonly left?: DocumentTransform; + /** + * @internal + * Used to iterate through all transforms that are concatenations or `split` links. + */ + readonly right?: DocumentTransform; } diff --git a/src/utilities/graphql/print.ts b/src/utilities/graphql/print.ts index e32a3f048df..20e779a9a55 100644 --- a/src/utilities/graphql/print.ts +++ b/src/utilities/graphql/print.ts @@ -5,6 +5,7 @@ import { cacheSizes, defaultCacheSizes, } from "../caching/index.js"; +import { registerGlobalCache } from "../caching/getMemoryInternals.js"; let printCache!: AutoCleanedWeakCache; export const print = Object.assign( @@ -25,5 +26,8 @@ export const print = Object.assign( }, } ); - print.reset(); + +if (__DEV__) { + registerGlobalCache("print", () => (printCache ? printCache.size : 0)); +} From 07fcf6a3bf5bc78ffe6f3e598897246b4da02cbb Mon Sep 17 00:00:00 2001 From: Simon Fletcher <102547495+sf-twingate@users.noreply.github.com> Date: Fri, 15 Dec 2023 11:06:05 -0700 Subject: [PATCH 59/90] feat: Allow returning `IGNORE` sentinel from `optimisticResponse` functions to support bailing-out from the optimistic update (#11410) --------- Co-authored-by: Alessia Bellisario --- .api-reports/api-report-core.md | 24 +++-- .api-reports/api-report-react.md | 34 +++++-- .api-reports/api-report-react_components.md | 34 +++++-- .api-reports/api-report-react_context.md | 34 +++++-- .api-reports/api-report-react_hoc.md | 34 +++++-- .api-reports/api-report-react_hooks.md | 34 +++++-- .api-reports/api-report-react_ssr.md | 34 +++++-- .api-reports/api-report-testing.md | 34 +++++-- .api-reports/api-report-testing_core.md | 34 +++++-- .api-reports/api-report-utilities.md | 24 +++-- .api-reports/api-report.md | 24 +++-- .changeset/mighty-coats-check.md | 47 +++++++++ .size-limits.json | 4 +- docs/shared/mutation-options.mdx | 4 +- src/__tests__/optimistic.ts | 107 ++++++++++++++++++++ src/cache/core/types/common.ts | 4 + src/core/QueryManager.ts | 23 +++-- src/core/watchQueryOptions.ts | 5 +- 18 files changed, 420 insertions(+), 118 deletions(-) create mode 100644 .changeset/mighty-coats-check.md diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 16c8ea5ed87..6d463558c91 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -1018,6 +1018,15 @@ export interface IdGetterObj extends Object { _id?: string; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) export interface IncrementalPayload { // (undocumented) @@ -1339,7 +1348,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; update?: MutationUpdaterFunction; updateQueries?: MutationQueryReducersMap; @@ -1763,7 +1774,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -2200,10 +2211,11 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:121:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:396:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:261:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:310:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 4b1402c6440..1dbb6cc93e9 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -908,6 +908,15 @@ export interface IDocumentDefinition { variables: ReadonlyArray; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -1167,7 +1176,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" @@ -1632,7 +1643,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -2305,22 +2316,23 @@ interface WatchQueryOptions> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -959,7 +968,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" @@ -1374,7 +1385,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1734,22 +1745,23 @@ interface WatchQueryOptions> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -930,7 +939,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" @@ -1282,7 +1293,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1630,22 +1641,23 @@ interface WatchQueryOptions> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -953,7 +962,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" @@ -1351,7 +1362,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1675,22 +1686,23 @@ export function withSubscription> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -1119,7 +1128,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" @@ -1550,7 +1561,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -2196,22 +2207,23 @@ interface WatchQueryOptions> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -916,7 +925,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" @@ -1268,7 +1279,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1616,22 +1627,23 @@ interface WatchQueryOptions> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -1030,7 +1039,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" @@ -1353,7 +1364,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1678,22 +1689,23 @@ export function withWarningSpy(it: (...args: TArgs // Warnings were encountered during analysis: // // src/cache/core/types/DataProxy.ts:141:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:96:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:97:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:98:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:99:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:121:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:396:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:261:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:310:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index dd02053d327..aceb1813901 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -723,6 +723,15 @@ interface GraphQLRequest> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -985,7 +994,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" @@ -1310,7 +1321,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1635,22 +1646,23 @@ export function withWarningSpy(it: (...args: TArgs // Warnings were encountered during analysis: // // src/cache/core/types/DataProxy.ts:141:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:96:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:97:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:98:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:99:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts -// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:121:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:396:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:261:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:310:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 6a64515659b..3c7390fc6c8 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -1254,6 +1254,15 @@ interface IdGetterObj extends Object { _id?: string; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) export type InclusionDirectives = Array<{ directive: DirectiveNode; @@ -1671,7 +1680,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" @@ -2115,7 +2126,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -2617,14 +2628,15 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:121:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:396:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:261:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:310:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/utilities/graphql/storeUtils.ts:226:12 - (ae-forgotten-export) The symbol "storeKeyNameStringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index ac97e197077..3892961f98a 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -1204,6 +1204,15 @@ export interface IDocumentDefinition { variables: ReadonlyArray; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) export interface IncrementalPayload { // (undocumented) @@ -1613,7 +1622,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; update?: MutationUpdaterFunction; updateQueries?: MutationQueryReducersMap; @@ -2193,7 +2204,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -2984,10 +2995,11 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:121:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:155:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:396:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:261:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:310:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:31:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts diff --git a/.changeset/mighty-coats-check.md b/.changeset/mighty-coats-check.md new file mode 100644 index 00000000000..0d80272f8a5 --- /dev/null +++ b/.changeset/mighty-coats-check.md @@ -0,0 +1,47 @@ +--- +"@apollo/client": minor +--- + +Allow returning `IGNORE` sentinel object from `optimisticResponse` functions to bail-out from the optimistic update. + +Consider this example: + +```jsx +const UPDATE_COMMENT = gql` + mutation UpdateComment($commentId: ID!, $commentContent: String!) { + updateComment(commentId: $commentId, content: $commentContent) { + id + __typename + content + } + } +`; + +function CommentPageWithData() { + const [mutate] = useMutation(UPDATE_COMMENT); + return ( + + mutate({ + variables: { commentId, commentContent }, + optimisticResponse: (vars, { IGNORE }) => { + if (commentContent === "foo") { + // conditionally bail out of optimistic updates + return IGNORE; + } + return { + updateComment: { + id: commentId, + __typename: "Comment", + content: commentContent + } + } + }, + }) + } + /> + ); +} +``` + +The `IGNORE` sentinel can be destructured from the second parameter in the callback function signature passed to `optimisticResponse`. diff --git a/.size-limits.json b/.size-limits.json index cffdc4320a2..19b14fb99a7 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38813, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32610 + "dist/apollo-client.min.cjs": 38900, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32683 } diff --git a/docs/shared/mutation-options.mdx b/docs/shared/mutation-options.mdx index fa22a6e6010..4357464ae9b 100644 --- a/docs/shared/mutation-options.mdx +++ b/docs/shared/mutation-options.mdx @@ -264,12 +264,12 @@ For more information, see [Updating the cache after a mutation](/react/data/muta ###### `optimisticResponse` -`Object` +`TData | (vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier }) => TData` -If provided, Apollo Client caches this temporary (and potentially incorrect) response until the mutation completes, enabling more responsive UI updates. +By providing either an object or a callback function that, when invoked after a mutation, allows you to return optimistic data and optionally skip updates via the `IGNORE` sentinel object, Apollo Client caches this temporary (and potentially incorrect) response until the mutation completes, enabling more responsive UI updates. For more information, see [Optimistic mutation results](/react/performance/optimistic-ui/). diff --git a/src/__tests__/optimistic.ts b/src/__tests__/optimistic.ts index 4184a2f8d6d..3cc984868ed 100644 --- a/src/__tests__/optimistic.ts +++ b/src/__tests__/optimistic.ts @@ -982,6 +982,113 @@ describe("optimistic mutation results", () => { resolve(); } ); + + itAsync( + "will not update optimistically if optimisticResponse returns IGNORE sentinel object", + async (resolve, reject) => { + expect.assertions(5); + + let subscriptionHandle: Subscription; + + const client = await setup(reject, { + request: { query: mutation, variables }, + result: mutationResult, + }); + + // we have to actually subscribe to the query to be able to update it + await new Promise((resolve) => { + const handle = client.watchQuery({ query }); + subscriptionHandle = handle.subscribe({ + next(res: any) { + resolve(res); + }, + }); + }); + + const id = "TodoList5"; + const isTodoList = ( + list: unknown + ): list is { todos: { text: string }[] } => + typeof initialList === "object" && + initialList !== null && + "todos" in initialList && + Array.isArray(initialList.todos); + + const initialList = client.cache.extract(true)[id]; + + if (!isTodoList(initialList)) { + reject(new Error("Expected TodoList")); + return; + } + + expect(initialList.todos.length).toEqual(3); + + const promise = client.mutate({ + mutation, + variables, + optimisticResponse: (vars, { IGNORE }) => { + return IGNORE; + }, + update: (proxy: any, mResult: any) => { + expect(mResult.data.createTodo.id).toBe("99"); + + const fragment = gql` + fragment todoList on TodoList { + todos { + id + text + completed + __typename + } + } + `; + + const data: any = proxy.readFragment({ id, fragment }); + + proxy.writeFragment({ + data: { + ...data, + todos: [mResult.data.createTodo, ...data.todos], + }, + id, + fragment, + }); + }, + }); + + const list = client.cache.extract(true)[id]; + + if (!isTodoList(list)) { + reject(new Error("Expected TodoList")); + return; + } + + expect(list.todos.length).toEqual(3); + + await promise; + + const result = await client.query({ query }); + + subscriptionHandle!.unsubscribe(); + + const newList = result.data.todoList; + + if (!isTodoList(newList)) { + reject(new Error("Expected TodoList")); + return; + } + + // There should be one more todo item than before + expect(newList.todos.length).toEqual(4); + + // Since we used `prepend` it should be at the front + expect(newList.todos[0].text).toBe( + "This one was created with a mutation." + ); + + resolve(); + } + ); }); describe("optimistic updates using `updateQueries`", () => { diff --git a/src/cache/core/types/common.ts b/src/cache/core/types/common.ts index 46c4fa8a155..886ccb63458 100644 --- a/src/cache/core/types/common.ts +++ b/src/cache/core/types/common.ts @@ -87,6 +87,10 @@ declare const _invalidateModifier: unique symbol; export interface InvalidateModifier { [_invalidateModifier]: true; } +declare const _ignoreModifier: unique symbol; +export interface IgnoreModifier { + [_ignoreModifier]: true; +} export type ModifierDetails = { DELETE: DeleteModifier; diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index ba70751e3c8..c8403d1420f 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -75,10 +75,13 @@ import { import type { ApolloErrorOptions } from "../errors/index.js"; import { PROTOCOL_ERRORS_SYMBOL } from "../errors/index.js"; import { print } from "../utilities/index.js"; +import type { IgnoreModifier } from "../cache/core/types/common.js"; import type { TODO } from "../utilities/types/TODO.js"; const { hasOwnProperty } = Object.prototype; +const IGNORE: IgnoreModifier = Object.create(null); + interface MutationStoreValue { mutation: DocumentNode; variables: Record; @@ -269,7 +272,8 @@ export class QueryManager { error: null, } as MutationStoreValue); - if (optimisticResponse) { + const isOptimistic = + optimisticResponse && this.markMutationOptimistic( optimisticResponse, { @@ -284,7 +288,6 @@ export class QueryManager { keepRootFields, } ); - } this.broadcastQueries(); @@ -296,7 +299,7 @@ export class QueryManager { mutation, { ...context, - optimisticResponse, + optimisticResponse: isOptimistic ? optimisticResponse : void 0, }, variables, false @@ -336,7 +339,7 @@ export class QueryManager { updateQueries, awaitRefetchQueries, refetchQueries, - removeOptimistic: optimisticResponse ? mutationId : void 0, + removeOptimistic: isOptimistic ? mutationId : void 0, onQueryUpdated, keepRootFields, }); @@ -361,7 +364,7 @@ export class QueryManager { mutationStoreValue.error = err; } - if (optimisticResponse) { + if (isOptimistic) { self.cache.removeOptimistic(mutationId); } @@ -611,10 +614,14 @@ export class QueryManager { ) { const data = typeof optimisticResponse === "function" ? - optimisticResponse(mutation.variables) + optimisticResponse(mutation.variables, { IGNORE }) : optimisticResponse; - return this.cache.recordOptimisticTransaction((cache) => { + if (data === IGNORE) { + return false; + } + + this.cache.recordOptimisticTransaction((cache) => { try { this.markMutationResult( { @@ -627,6 +634,8 @@ export class QueryManager { invariant.error(error); } }, mutation.mutationId); + + return true; } public fetchQuery( diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index 7c49d861097..fc722c5ed9c 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -12,6 +12,7 @@ import type { } from "./types.js"; import type { ApolloCache } from "../cache/index.js"; import type { ObservableQuery } from "./ObservableQuery.js"; +import type { IgnoreModifier } from "../cache/core/types/common.js"; /** * fetchPolicy determines where the client may return a result from. The options are: @@ -272,7 +273,9 @@ export interface MutationBaseOptions< * the result of a mutation immediately, and update the UI later if any errors * appear. */ - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: + | TData + | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier }) => TData); /** * A {@link MutationQueryReducersMap}, which is map from query names to From 58db5c3295b88162f91019f0898f6baa4b9cced6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 18 Dec 2023 10:07:31 -0700 Subject: [PATCH 60/90] Add the ability to preload a query outside of React (#11412) Co-authored-by: Lenz Weber-Tronic Co-authored-by: jerelmiller --- .api-reports/api-report-core.md | 4 + .api-reports/api-report-react.md | 106 +- .api-reports/api-report-react_components.md | 4 + .api-reports/api-report-react_context.md | 4 + .api-reports/api-report-react_hoc.md | 4 + .api-reports/api-report-react_hooks.md | 48 +- .api-reports/api-report-react_ssr.md | 4 + .api-reports/api-report-testing.md | 4 + .api-reports/api-report-testing_core.md | 4 + .api-reports/api-report-utilities.md | 4 + .api-reports/api-report.md | 103 +- .changeset/dirty-tigers-matter.md | 13 + .changeset/rare-snakes-melt.md | 24 + .size-limits.json | 4 +- config/inlineInheritDoc.ts | 6 +- config/jest.config.js | 2 + src/__tests__/__snapshots__/exports.ts.snap | 5 + src/core/ObservableQuery.ts | 5 + src/core/QueryInfo.ts | 4 + src/react/cache/QueryReference.ts | 178 +- .../cache/__tests__/QueryReference.test.ts | 27 + .../__tests__/useBackgroundQuery.test.tsx | 62 +- .../__tests__/useQueryRefHandlers.test.tsx | 1886 +++++++++++++ src/react/hooks/index.ts | 2 + src/react/hooks/useBackgroundQuery.ts | 26 +- src/react/hooks/useLoadableQuery.ts | 11 +- src/react/hooks/useQueryRefHandlers.ts | 87 + src/react/hooks/useReadQuery.ts | 15 +- src/react/index.ts | 7 + .../__tests__/createQueryPreloader.test.tsx | 2508 +++++++++++++++++ .../query-preloader/createQueryPreloader.ts | 193 ++ src/testing/internal/index.ts | 19 + src/testing/internal/renderHelpers.tsx | 70 + src/testing/internal/scenarios/index.ts | 108 + src/testing/matchers/index.d.ts | 6 + src/testing/matchers/index.ts | 2 + src/testing/matchers/toBeDisposed.ts | 35 + 37 files changed, 5441 insertions(+), 153 deletions(-) create mode 100644 .changeset/dirty-tigers-matter.md create mode 100644 .changeset/rare-snakes-melt.md create mode 100644 src/react/cache/__tests__/QueryReference.test.ts create mode 100644 src/react/hooks/__tests__/useQueryRefHandlers.test.tsx create mode 100644 src/react/hooks/useQueryRefHandlers.ts create mode 100644 src/react/query-preloader/__tests__/createQueryPreloader.test.tsx create mode 100644 src/react/query-preloader/createQueryPreloader.ts create mode 100644 src/testing/internal/renderHelpers.tsx create mode 100644 src/testing/internal/scenarios/index.ts create mode 100644 src/testing/matchers/toBeDisposed.ts diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 6d463558c91..1e0a35ce6ed 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -1519,6 +1519,8 @@ export class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1690,6 +1692,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 1dbb6cc93e9..b9a8d200c9d 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -556,6 +556,9 @@ type ConcastSourcesIterable = Iterable>; export interface Context extends Record { } +// @public +export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -936,13 +939,15 @@ interface IncrementalPayload { // @public (undocumented) class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts - constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); + constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); // (undocumented) applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts // // (undocumented) didChangeOptions(watchQueryOptions: ObservedOptions): boolean; + // (undocumented) + get disposed(): boolean; // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -964,6 +969,8 @@ class InternalQueryReference { // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) + reinitialize(): void; + // (undocumented) result: ApolloQueryResult; // (undocumented) retain(): () => void; @@ -1347,6 +1354,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1451,6 +1460,47 @@ interface PendingPromise extends Promise { status: "pending"; } +// @public (undocumented) +export type PreloadQueryFetchPolicy = Extract; + +// @public +export interface PreloadQueryFunction { + // Warning: (ae-forgotten-export) The symbol "PreloadQueryOptionsArg" needs to be exported by the entry point index.d.ts + >(query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg, TOptions>): QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + }): QueryReference | undefined, TVariables>; + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + errorPolicy: "ignore" | "all"; + }): QueryReference; + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + returnPartialData: true; + }): QueryReference, TVariables>; + (query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg>): QueryReference; +} + +// Warning: (ae-forgotten-export) The symbol "VariablesOption" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type PreloadQueryOptions = { + canonizeResults?: boolean; + context?: Context; + errorPolicy?: ErrorPolicy; + fetchPolicy?: PreloadQueryFetchPolicy; + returnPartialData?: boolean; + refetchWritePolicy?: RefetchWritePolicy; +} & VariablesOption; + +// @public (undocumented) +type PreloadQueryOptionsArg = [TVariables] extends [never] ? [ +options?: PreloadQueryOptions & TOptions +] : {} extends OnlyRequiredProperties ? [ +options?: PreloadQueryOptions> & Omit +] : [ +options: PreloadQueryOptions> & Omit +]; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1543,6 +1593,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1716,13 +1768,15 @@ interface QueryOptions { // Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "useBackgroundQuery" has more than one declaration; you need to add a TSDoc member reference selector // // @public -export interface QueryReference { +export interface QueryReference { // (undocumented) [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // // (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // (undocumented) + toPromise(): Promise>; } // Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts @@ -2072,7 +2126,7 @@ export function useApolloClient(override?: ApolloClient): ApolloClient, "variables">>(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer & TOptions): [ -(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData> | (TOptions["skip"] extends boolean ? undefined : never)), +(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables> | (TOptions["skip"] extends boolean ? undefined : never)), UseBackgroundQueryResult ]; @@ -2081,7 +2135,7 @@ export function useBackgroundQuery | undefined>, +QueryReference | undefined, TVariables>, UseBackgroundQueryResult ]; @@ -2089,7 +2143,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { errorPolicy: "ignore" | "all"; }): [ -QueryReference, +QueryReference, UseBackgroundQueryResult ]; @@ -2098,7 +2152,7 @@ export function useBackgroundQuery> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; @@ -2106,7 +2160,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; }): [ -QueryReference>, +QueryReference, TVariables>, UseBackgroundQueryResult ]; @@ -2114,12 +2168,15 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { skip: boolean; }): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; // @public (undocumented) -export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [QueryReference, UseBackgroundQueryResult]; +export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [ +QueryReference, +UseBackgroundQueryResult +]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken): [undefined, UseBackgroundQueryResult]; @@ -2128,13 +2185,13 @@ export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken | (BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; })): [ -QueryReference> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; @@ -2194,7 +2251,7 @@ export function useLoadableQuery = [ LoadQueryFunction, -QueryReference | null, +QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -2208,6 +2265,17 @@ export function useMutation(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; +// @public +export function useQueryRefHandlers(queryRef: QueryReference): UseQueryRefHandlersResult; + +// @public (undocumented) +export interface UseQueryRefHandlersResult { + // (undocumented) + fetchMore: FetchMoreFunction; + + refetch: RefetchFunction; +} + // Warning: (ae-forgotten-export) The symbol "ReactiveVar" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -2287,6 +2355,17 @@ export interface UseSuspenseQueryResult; } +// @public (undocumented) +type VariablesOption = [ +TVariables +] extends [never] ? { + variables?: Record; +} : {} extends OnlyRequiredProperties ? { + variables?: TVariables; +} : { + variables: TVariables; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; @@ -2336,6 +2415,9 @@ interface WatchQueryOptions>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1299,6 +1301,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; diff --git a/.api-reports/api-report-react_context.md b/.api-reports/api-report-react_context.md index 3f2234e460b..dfe46b052f2 100644 --- a/.api-reports/api-report-react_context.md +++ b/.api-reports/api-report-react_context.md @@ -1072,6 +1072,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1207,6 +1209,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; diff --git a/.api-reports/api-report-react_hoc.md b/.api-reports/api-report-react_hoc.md index 648b7c58501..6ceb34f8ed7 100644 --- a/.api-reports/api-report-react_hoc.md +++ b/.api-reports/api-report-react_hoc.md @@ -1124,6 +1124,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1276,6 +1278,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index ec960e2197d..012dd682bb1 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -883,13 +883,15 @@ interface IncrementalPayload { // @public (undocumented) class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts - constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); + constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); // (undocumented) applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts // // (undocumented) didChangeOptions(watchQueryOptions: ObservedOptions): boolean; + // (undocumented) + get disposed(): boolean; // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -911,6 +913,8 @@ class InternalQueryReference { // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) + reinitialize(): void; + // (undocumented) result: ApolloQueryResult; // (undocumented) retain(): () => void; @@ -1295,6 +1299,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1469,6 +1475,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1634,13 +1642,15 @@ interface QueryOptions { // Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "useBackgroundQuery" has more than one declaration; you need to add a TSDoc member reference selector // // @public -interface QueryReference { +interface QueryReference { // (undocumented) [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // // (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // (undocumented) + toPromise(): Promise>; } // Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts @@ -1952,7 +1962,7 @@ export function useApolloClient(override?: ApolloClient): ApolloClient, "variables">>(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer & TOptions): [ -(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData> | (TOptions["skip"] extends boolean ? undefined : never)), +(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables> | (TOptions["skip"] extends boolean ? undefined : never)), UseBackgroundQueryResult ]; @@ -1961,7 +1971,7 @@ export function useBackgroundQuery | undefined>, +QueryReference | undefined, TVariables>, UseBackgroundQueryResult ]; @@ -1969,7 +1979,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { errorPolicy: "ignore" | "all"; }): [ -QueryReference, +QueryReference, UseBackgroundQueryResult ]; @@ -1978,7 +1988,7 @@ export function useBackgroundQuery> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; @@ -1986,7 +1996,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; }): [ -QueryReference>, +QueryReference, TVariables>, UseBackgroundQueryResult ]; @@ -1994,12 +2004,15 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { skip: boolean; }): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; // @public (undocumented) -export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [QueryReference, UseBackgroundQueryResult]; +export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [ +QueryReference, +UseBackgroundQueryResult +]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken): [undefined, UseBackgroundQueryResult]; @@ -2008,13 +2021,13 @@ export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken | (BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; })): [ -QueryReference> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; @@ -2078,7 +2091,7 @@ export function useLoadableQuery = [ LoadQueryFunction, -QueryReference | null, +QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -2095,6 +2108,17 @@ export function useMutation(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; +// @public +export function useQueryRefHandlers(queryRef: QueryReference): UseQueryRefHandlersResult; + +// @public (undocumented) +export interface UseQueryRefHandlersResult { + // (undocumented) + fetchMore: FetchMoreFunction; + + refetch: RefetchFunction; +} + // Warning: (ae-forgotten-export) The symbol "ReactiveVar" needs to be exported by the entry point index.d.ts // // @public (undocumented) diff --git a/.api-reports/api-report-react_ssr.md b/.api-reports/api-report-react_ssr.md index b05e11fdc52..c5aac234fd5 100644 --- a/.api-reports/api-report-react_ssr.md +++ b/.api-reports/api-report-react_ssr.md @@ -1058,6 +1058,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1193,6 +1195,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index 1d3b7835348..2ba6c4bf422 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -1182,6 +1182,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1280,6 +1282,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index aceb1813901..98b95dd615f 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -1137,6 +1137,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1235,6 +1237,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 3c7390fc6c8..5ebbd459f66 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -1856,6 +1856,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Promise>; // (undocumented) reobserveAsConcast(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -2040,6 +2042,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 3892961f98a..acd0311dda6 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -556,6 +556,9 @@ export const concat: typeof ApolloLink.concat; // @public (undocumented) export const createHttpLink: (linkOptions?: HttpOptions) => ApolloLink; +// @public +export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; + // @public @deprecated (undocumented) export const createSignalIfSupported: () => { controller: boolean; @@ -1306,13 +1309,15 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { // @public (undocumented) class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts - constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); + constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); // (undocumented) applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts // // (undocumented) didChangeOptions(watchQueryOptions: ObservedOptions): boolean; + // (undocumented) + get disposed(): boolean; // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions_2" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1334,6 +1339,8 @@ class InternalQueryReference { // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) + reinitialize(): void; + // (undocumented) result: ApolloQueryResult; // (undocumented) retain(): () => void; @@ -1837,6 +1844,8 @@ export class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -2003,6 +2012,47 @@ export type PossibleTypesMap = { [supertype: string]: string[]; }; +// @public (undocumented) +export type PreloadQueryFetchPolicy = Extract; + +// @public +export interface PreloadQueryFunction { + // Warning: (ae-forgotten-export) The symbol "PreloadQueryOptionsArg" needs to be exported by the entry point index.d.ts + >(query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg, TOptions>): QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + }): QueryReference | undefined, TVariables>; + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + errorPolicy: "ignore" | "all"; + }): QueryReference; + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + returnPartialData: true; + }): QueryReference, TVariables>; + (query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg>): QueryReference; +} + +// Warning: (ae-forgotten-export) The symbol "VariablesOption" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type PreloadQueryOptions = { + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: PreloadQueryFetchPolicy; + returnPartialData?: boolean; + refetchWritePolicy?: RefetchWritePolicy; +} & VariablesOption; + +// @public (undocumented) +type PreloadQueryOptionsArg = [TVariables] extends [never] ? [ +options?: PreloadQueryOptions & TOptions +] : {} extends OnlyRequiredProperties ? [ +options?: PreloadQueryOptions> & Omit +] : [ +options: PreloadQueryOptions> & Omit +]; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -2106,6 +2156,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -2274,13 +2326,15 @@ export { QueryOptions } // Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "useBackgroundQuery" has more than one declaration; you need to add a TSDoc member reference selector // // @public -export interface QueryReference { +export interface QueryReference { // (undocumented) [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // // (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // (undocumented) + toPromise(): Promise>; } // Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts @@ -2720,7 +2774,7 @@ export function useApolloClient(override?: ApolloClient): ApolloClient, "variables">>(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer & TOptions): [ -(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData> | (TOptions["skip"] extends boolean ? undefined : never)), +(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables> | (TOptions["skip"] extends boolean ? undefined : never)), UseBackgroundQueryResult ]; @@ -2729,7 +2783,7 @@ export function useBackgroundQuery | undefined>, +QueryReference | undefined, TVariables>, UseBackgroundQueryResult ]; @@ -2737,7 +2791,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { errorPolicy: "ignore" | "all"; }): [ -QueryReference, +QueryReference, UseBackgroundQueryResult ]; @@ -2746,7 +2800,7 @@ export function useBackgroundQuery> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; @@ -2754,7 +2808,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; }): [ -QueryReference>, +QueryReference, TVariables>, UseBackgroundQueryResult ]; @@ -2762,12 +2816,15 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { skip: boolean; }): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; // @public (undocumented) -export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [QueryReference, UseBackgroundQueryResult]; +export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [ +QueryReference, +UseBackgroundQueryResult +]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken): [undefined, UseBackgroundQueryResult]; @@ -2776,13 +2833,13 @@ export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken | (BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; })): [ -QueryReference> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; @@ -2842,7 +2899,7 @@ export function useLoadableQuery = [ LoadQueryFunction, -QueryReference | null, +QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -2856,6 +2913,17 @@ export function useMutation(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; +// @public +export function useQueryRefHandlers(queryRef: QueryReference): UseQueryRefHandlersResult; + +// @public (undocumented) +export interface UseQueryRefHandlersResult { + // (undocumented) + fetchMore: FetchMoreFunction; + + refetch: RefetchFunction; +} + // @public (undocumented) export function useReactiveVar(rv: ReactiveVar): T; @@ -2933,6 +3001,17 @@ export interface UseSuspenseQueryResult; } +// @public (undocumented) +type VariablesOption = [ +TVariables +] extends [never] ? { + variables?: Record; +} : {} extends OnlyRequiredProperties ? { + variables?: TVariables; +} : { + variables: TVariables; +}; + // @public (undocumented) export type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.changeset/dirty-tigers-matter.md b/.changeset/dirty-tigers-matter.md new file mode 100644 index 00000000000..1a5d4a9e195 --- /dev/null +++ b/.changeset/dirty-tigers-matter.md @@ -0,0 +1,13 @@ +--- +"@apollo/client": minor +--- + +Create a new `useQueryRefHandlers` hook that returns `refetch` and `fetchMore` functions for a given `queryRef`. This is useful to get access to handlers for a `queryRef` that was created by `createQueryPreloader` or when the handlers for a `queryRef` produced by a different component are inaccessible. + +```jsx +const MyComponent({ queryRef }) { + const { refetch, fetchMore } = useQueryRefHandlers(queryRef); + + // ... +} +``` diff --git a/.changeset/rare-snakes-melt.md b/.changeset/rare-snakes-melt.md new file mode 100644 index 00000000000..6757b401a47 --- /dev/null +++ b/.changeset/rare-snakes-melt.md @@ -0,0 +1,24 @@ +--- +"@apollo/client": minor +--- + +Add the ability to start preloading a query outside React to begin fetching as early as possible. Call `createQueryPreloader` to create a `preloadQuery` function which can be called to start fetching a query. This returns a `queryRef` which is passed to `useReadQuery` and suspended until the query is done fetching. + +```tsx +const preloadQuery = createQueryPreloader(client); +const queryRef = preloadQuery(QUERY, { variables, ...otherOptions }); + +function App() { + return { + Loading}> + + + } +} + +function MyQuery() { + const { data } = useReadQuery(queryRef); + + // do something with data +} +``` diff --git a/.size-limits.json b/.size-limits.json index 19b14fb99a7..dea1c9e18db 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38900, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32683 + "dist/apollo-client.min.cjs": 39137, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32653 } diff --git a/config/inlineInheritDoc.ts b/config/inlineInheritDoc.ts index 5222dafde7a..5c94a53363c 100644 --- a/config/inlineInheritDoc.ts +++ b/config/inlineInheritDoc.ts @@ -128,7 +128,11 @@ function processComments() { const sourceFiles = project.addSourceFilesAtPaths("dist/**/*.d.ts"); for (const file of sourceFiles) { file.forEachDescendant((node) => { - if (Node.isPropertySignature(node)) { + if ( + Node.isPropertySignature(node) || + Node.isMethodSignature(node) || + Node.isCallSignatureDeclaration(node) + ) { const docsNode = node.getJsDocs()[0]; if (!docsNode) return; const oldText = docsNode.getInnerText(); diff --git a/config/jest.config.js b/config/jest.config.js index a45df96fc48..6851e2a6e06 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -36,6 +36,8 @@ const react17TestFileIgnoreList = [ "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", "src/react/hooks/__tests__/useLoadableQuery.test.tsx", + "src/react/hooks/__tests__/useQueryRefHandlers.test.tsx", + "src/react/query-preloader/__tests__/createQueryPreloader.test.tsx", ]; const tsStandardConfig = { diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index ba76b853b7c..d51ea5ff2ad 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -20,6 +20,7 @@ Array [ "checkFetcher", "concat", "createHttpLink", + "createQueryPreloader", "createSignalIfSupported", "defaultDataIdFromObject", "defaultPrinter", @@ -62,6 +63,7 @@ Array [ "useLoadableQuery", "useMutation", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", @@ -265,6 +267,7 @@ Array [ "ApolloConsumer", "ApolloProvider", "DocumentType", + "createQueryPreloader", "getApolloContext", "operationName", "parser", @@ -277,6 +280,7 @@ Array [ "useLoadableQuery", "useMutation", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", @@ -321,6 +325,7 @@ Array [ "useLoadableQuery", "useMutation", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 4822807e565..5cd6189b84d 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -225,6 +225,11 @@ export class ObservableQuery< }); } + /** @internal */ + public resetDiff() { + this.queryInfo.resetDiff(); + } + public getCurrentResult(saveAsLastResult = true): ApolloQueryResult { // Use the last result as long as the variables match this.variables. const lastResult = this.getLastResult(true); diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index b3dd4366cf2..f2aa2afa518 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -157,6 +157,10 @@ export class QueryInfo { this.dirty = false; } + resetDiff() { + this.lastDiff = void 0; + } + getDiff(): Cache.DiffResult { const options = this.getDiffOptions(); diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 97025c37335..65b11f929fc 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -6,7 +6,6 @@ import type { OperationVariables, WatchQueryOptions, } from "../../core/index.js"; -import { isNetworkRequestSettled } from "../../core/index.js"; import type { ObservableSubscription, PromiseWithState, @@ -35,9 +34,10 @@ const PROMISE_SYMBOL: unique symbol = Symbol(); * A child component reading the `QueryReference` via {@link useReadQuery} will * suspend until the promise resolves. */ -export interface QueryReference { +export interface QueryReference { readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; [PROMISE_SYMBOL]: QueryRefPromise; + toPromise(): Promise>; } interface InternalQueryReferenceOptions { @@ -45,30 +45,43 @@ interface InternalQueryReferenceOptions { autoDisposeTimeoutMs?: number; } -export function wrapQueryRef( +export function wrapQueryRef( internalQueryRef: InternalQueryReference -): QueryReference { - return { +) { + const ref: QueryReference = { + toPromise() { + // We avoid resolving this promise with the query data because we want to + // discourage using the server data directly from the queryRef. Instead, + // the data should be accessed through `useReadQuery`. When the server + // data is needed, its better to use `client.query()` directly. + // + // Here we resolve with the ref itself to make using this in React Router + // or TanStack Router `loader` functions a bit more ergonomic e.g. + // + // function loader() { + // return { queryRef: await preloadQuery(query).toPromise() } + // } + return getWrappedPromise(ref).then(() => ref); + }, [QUERY_REFERENCE_SYMBOL]: internalQueryRef, [PROMISE_SYMBOL]: internalQueryRef.promise, }; + + return ref; +} + +export function getWrappedPromise(queryRef: QueryReference) { + const internalQueryRef = unwrapQueryRef(queryRef); + + return internalQueryRef.promise.status === "fulfilled" ? + internalQueryRef.promise + : queryRef[PROMISE_SYMBOL]; } export function unwrapQueryRef( queryRef: QueryReference -): [InternalQueryReference, () => QueryRefPromise] { - const internalQueryRef = queryRef[QUERY_REFERENCE_SYMBOL]; - - return [ - internalQueryRef, - () => - // There is a chance the query ref's promise has been updated in the time - // the original promise had been suspended. In that case, we want to use - // it instead of the older promise which may contain outdated data. - internalQueryRef.promise.status === "fulfilled" ? - internalQueryRef.promise - : queryRef[PROMISE_SYMBOL], - ]; +): InternalQueryReference { + return queryRef[QUERY_REFERENCE_SYMBOL]; } export function updateWrappedQueryRef( @@ -93,16 +106,15 @@ type ObservedOptions = Pick< >; export class InternalQueryReference { - public result: ApolloQueryResult; + public result!: ApolloQueryResult; public readonly key: QueryKey = {}; public readonly observable: ObservableQuery; - public promise: QueryRefPromise; + public promise!: QueryRefPromise; - private subscription: ObservableSubscription; + private subscription!: ObservableSubscription; private listeners = new Set>(); private autoDisposeTimeoutId?: NodeJS.Timeout; - private status: "idle" | "loading" = "loading"; private resolve: ((result: ApolloQueryResult) => void) | undefined; private reject: ((error: unknown) => void) | undefined; @@ -110,43 +122,20 @@ export class InternalQueryReference { private references = 0; constructor( - observable: ObservableQuery, + observable: ObservableQuery, options: InternalQueryReferenceOptions ) { this.handleNext = this.handleNext.bind(this); this.handleError = this.handleError.bind(this); this.dispose = this.dispose.bind(this); this.observable = observable; - // Don't save this result as last result to prevent delivery of last result - // when first subscribing - this.result = observable.getCurrentResult(false); if (options.onDispose) { this.onDispose = options.onDispose; } - if ( - isNetworkRequestSettled(this.result.networkStatus) || - (this.result.data && - (!this.result.partial || this.watchQueryOptions.returnPartialData)) - ) { - this.promise = createFulfilledPromise(this.result); - this.status = "idle"; - } else { - this.promise = wrapPromiseWithState( - new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }) - ); - } - - this.subscription = observable - .filter(({ data }) => !equal(data, {})) - .subscribe({ - next: this.handleNext, - error: this.handleError, - }); + this.setResult(); + this.subscribeToQuery(); // Start a timer that will automatically dispose of the query if the // suspended resource does not use this queryRef in the given time. This @@ -167,10 +156,40 @@ export class InternalQueryReference { this.promise.then(startDisposeTimer, startDisposeTimer); } + get disposed() { + return this.subscription.closed; + } + get watchQueryOptions() { return this.observable.options; } + reinitialize() { + const { observable } = this; + + const originalFetchPolicy = this.watchQueryOptions.fetchPolicy; + + try { + if (originalFetchPolicy !== "no-cache") { + observable.resetLastResults(); + observable.silentSetOptions({ fetchPolicy: "cache-first" }); + } else { + observable.silentSetOptions({ fetchPolicy: "standby" }); + } + + this.subscribeToQuery(); + + if (originalFetchPolicy === "no-cache") { + return; + } + + observable.resetDiff(); + this.setResult(); + } finally { + observable.silentSetOptions({ fetchPolicy: originalFetchPolicy }); + } + } + retain() { this.references++; clearTimeout(this.autoDisposeTimeoutId); @@ -251,19 +270,18 @@ export class InternalQueryReference { } private handleNext(result: ApolloQueryResult) { - switch (this.status) { - case "loading": { + switch (this.promise.status) { + case "pending": { // Maintain the last successful `data` value if the next result does not // have one. if (result.data === void 0) { result.data = this.result.data; } - this.status = "idle"; this.result = result; this.resolve?.(result); break; } - case "idle": { + default: { // This occurs when switching to a result that is fully cached when this // class is instantiated. ObservableQuery will run reobserve when // subscribing, which delivers a result from the cache. @@ -292,13 +310,12 @@ export class InternalQueryReference { this.handleError ); - switch (this.status) { - case "loading": { - this.status = "idle"; + switch (this.promise.status) { + case "pending": { this.reject?.(error); break; } - case "idle": { + default: { this.promise = createRejectedPromise>(error); this.deliver(this.promise); } @@ -310,15 +327,7 @@ export class InternalQueryReference { } private initiateFetch(returnedPromise: Promise>) { - this.status = "loading"; - - this.promise = wrapPromiseWithState( - new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }) - ); - + this.promise = this.createPendingPromise(); this.promise.catch(() => {}); // If the data returned from the fetch is deeply equal to the data already @@ -328,8 +337,7 @@ export class InternalQueryReference { // promise is resolved correctly. returnedPromise .then((result) => { - if (this.status === "loading") { - this.status = "idle"; + if (this.promise.status === "pending") { this.result = result; this.resolve?.(result); } @@ -338,4 +346,40 @@ export class InternalQueryReference { return returnedPromise; } + + private subscribeToQuery() { + this.subscription = this.observable + .filter( + (result) => !equal(result.data, {}) && !equal(result, this.result) + ) + .subscribe(this.handleNext, this.handleError); + } + + private setResult() { + // Don't save this result as last result to prevent delivery of last result + // when first subscribing + const result = this.observable.getCurrentResult(false); + + if (equal(result, this.result)) { + return; + } + + this.result = result; + this.promise = + ( + result.data && + (!result.partial || this.watchQueryOptions.returnPartialData) + ) ? + createFulfilledPromise(result) + : this.createPendingPromise(); + } + + private createPendingPromise() { + return wrapPromiseWithState( + new Promise>((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }) + ); + } } diff --git a/src/react/cache/__tests__/QueryReference.test.ts b/src/react/cache/__tests__/QueryReference.test.ts new file mode 100644 index 00000000000..f520a9a2e53 --- /dev/null +++ b/src/react/cache/__tests__/QueryReference.test.ts @@ -0,0 +1,27 @@ +import { + ApolloClient, + ApolloLink, + InMemoryCache, + Observable, +} from "../../../core"; +import { setupSimpleCase } from "../../../testing/internal"; +import { InternalQueryReference } from "../QueryReference"; + +test("kicks off request immediately when created", async () => { + const { query } = setupSimpleCase(); + let fetchCount = 0; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink((operation) => { + fetchCount++; + return Observable.of({ data: { greeting: "Hello" } }); + }), + }); + + const observable = client.watchQuery({ query }); + + expect(fetchCount).toBe(0); + new InternalQueryReference(observable, {}); + expect(fetchCount).toBe(1); +}); diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 85b857e47a5..75d07621029 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -44,7 +44,7 @@ import { import { useBackgroundQuery } from "../useBackgroundQuery"; import { useReadQuery } from "../useReadQuery"; import { ApolloProvider } from "../../context"; -import { unwrapQueryRef, QueryReference } from "../../cache/QueryReference"; +import { QueryReference, getWrappedPromise } from "../../cache/QueryReference"; import { InMemoryCache } from "../../../cache"; import { SuspenseQueryHookFetchPolicy, @@ -643,7 +643,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await getWrappedPromise(queryRef); expect(_result).toEqual({ data: { hello: "world 1" }, @@ -680,7 +680,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await getWrappedPromise(queryRef); await waitFor(() => { expect(_result).toEqual({ @@ -721,7 +721,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await getWrappedPromise(queryRef); await waitFor(() => { expect(_result).toMatchObject({ @@ -781,7 +781,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await getWrappedPromise(queryRef); const resultSet = new Set(_result.data.results); const values = Array.from(resultSet).map((item) => item.value); @@ -842,7 +842,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await getWrappedPromise(queryRef); const resultSet = new Set(_result.data.results); const values = Array.from(resultSet).map((item) => item.value); @@ -884,7 +884,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await getWrappedPromise(queryRef); expect(_result).toEqual({ data: { hello: "from link" }, @@ -924,7 +924,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await getWrappedPromise(queryRef); expect(_result).toEqual({ data: { hello: "from cache" }, @@ -971,7 +971,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await getWrappedPromise(queryRef); expect(_result).toEqual({ data: { foo: "bar", hello: "from link" }, @@ -1011,7 +1011,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await getWrappedPromise(queryRef); expect(_result).toEqual({ data: { hello: "from link" }, @@ -1054,7 +1054,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await getWrappedPromise(queryRef); expect(_result).toEqual({ data: { hello: "from link" }, @@ -5661,7 +5661,7 @@ describe("useBackgroundQuery", () => { }); expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); expectTypeOf(inferredQueryRef).not.toEqualTypeOf< QueryReference @@ -5673,10 +5673,10 @@ describe("useBackgroundQuery", () => { >(query, { skip: true }); expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); // TypeScript is too smart and using a `const` or `let` boolean variable @@ -5691,10 +5691,10 @@ describe("useBackgroundQuery", () => { }); expectTypeOf(dynamicQueryRef).toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); expectTypeOf(dynamicQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); }); @@ -5705,7 +5705,7 @@ describe("useBackgroundQuery", () => { expectTypeOf(inferredQueryRef).toEqualTypeOf(); expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); const [explicitQueryRef] = useBackgroundQuery< @@ -5715,7 +5715,7 @@ describe("useBackgroundQuery", () => { expectTypeOf(explicitQueryRef).toEqualTypeOf(); expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); }); @@ -5731,10 +5731,10 @@ describe("useBackgroundQuery", () => { ); expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); const [explicitQueryRef] = useBackgroundQuery< @@ -5743,10 +5743,10 @@ describe("useBackgroundQuery", () => { >(query, options.skip ? skipToken : undefined); expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); }); @@ -5762,22 +5762,24 @@ describe("useBackgroundQuery", () => { ); expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference> | undefined + | QueryReference, VariablesCaseVariables> + | undefined >(); expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); - const [explicitQueryRef] = useBackgroundQuery( - query, - options.skip ? skipToken : { returnPartialData: true } - ); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, options.skip ? skipToken : { returnPartialData: true }); expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference> | undefined + | QueryReference, VariablesCaseVariables> + | undefined >(); expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); }); }); diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx new file mode 100644 index 00000000000..37734d6bd20 --- /dev/null +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -0,0 +1,1886 @@ +import React from "react"; +import { act, render, screen } from "@testing-library/react"; +import { + ApolloClient, + InMemoryCache, + NetworkStatus, + TypedDocumentNode, + gql, +} from "../../../core"; +import { MockLink, MockedResponse } from "../../../testing"; +import { + PaginatedCaseData, + SimpleCaseData, + createProfiler, + renderWithClient, + setupPaginatedCase, + setupSimpleCase, + useTrackRenders, +} from "../../../testing/internal"; +import { useQueryRefHandlers } from "../useQueryRefHandlers"; +import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; +import { Suspense } from "react"; +import { createQueryPreloader } from "../../query-preloader/createQueryPreloader"; +import userEvent from "@testing-library/user-event"; +import { QueryReference } from "../../cache/QueryReference"; +import { useBackgroundQuery } from "../useBackgroundQuery"; +import { useLoadableQuery } from "../useLoadableQuery"; +import { concatPagination } from "../../../utilities"; + +test("does not interfere with updates from useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + // We can ignore the return result here since we are testing the mechanics + // of this hook to ensure it doesn't interfere with the updates from + // useReadQuery + useQueryRefHandlers(queryRef); + + return ( + }> + + + ); + } + + const { rerender } = renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ query, data: { greeting: "Hello again" } }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + rerender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("refetches and resuspends when calling refetch", async () => { + const { query, mocks: defaultMocks } = setupSimpleCase(); + + const user = userEvent.setup(); + + const mocks = [ + defaultMocks[0], + { + request: { query }, + result: { data: { greeting: "Hello again" } }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { refetch } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test('honors refetchWritePolicy set to "merge"', async () => { + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + interface QueryData { + primes: number[]; + } + + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + delay: 10, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + ]; + + const mergeParams: [number[] | undefined, number[]][] = []; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, + }, + }, + }); + + const user = userEvent.setup(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { + refetchWritePolicy: "merge", + variables: { min: 0, max: 12 }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { refetch } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + ]); + } + + await expect(Profiler).not.toRerender(); +}); + +test('honors refetchWritePolicy set to "overwrite"', async () => { + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + interface QueryData { + primes: number[]; + } + + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + delay: 10, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + ]; + + const mergeParams: [number[] | undefined, number[]][] = []; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, + }, + }, + }); + + const user = userEvent.setup(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { + refetchWritePolicy: "overwrite", + variables: { min: 0, max: 12 }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { refetch } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [undefined, [13, 17, 19, 23, 29]], + ]); + } + + await expect(Profiler).not.toRerender(); +}); + +test('defaults refetchWritePolicy to "overwrite"', async () => { + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + interface QueryData { + primes: number[]; + } + + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + delay: 10, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + ]; + + const mergeParams: [number[] | undefined, number[]][] = []; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, + }, + }, + }); + + const user = userEvent.setup(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { + variables: { min: 0, max: 12 }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { refetch } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [undefined, [13, 17, 19, 23, 29]], + ]); + } + + await expect(Profiler).not.toRerender(); +}); + +test("`refetch` works with startTransition", async () => { + type Variables = { + id: string; + }; + + interface Data { + todo: { + id: string; + name: string; + completed: boolean; + }; + } + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { variables: { id: "1" } }); + + function App() { + useTrackRenders(); + const { refetch } = useQueryRefHandlers(queryRef); + const [isPending, startTransition] = React.useTransition(); + + Profiler.mergeSnapshot({ isPending }); + + return ( + <> + + }> + + + + ); + } + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function Todo() { + useTrackRenders(); + const result = useReadQuery(queryRef); + const { todo } = result.data; + + Profiler.mergeSnapshot({ result }); + + return ( +
+ {todo.name} + {todo.completed && " (completed)"} +
+ ); + } + + render(, { wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + const button = screen.getByText("Refetch"); + await act(() => user.click(button)); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, Todo]); + expect(snapshot).toEqual({ + isPending: true, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, Todo]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("`refetch` works with startTransition from useBackgroundQuery and usePreloadedQueryHandlers", async () => { + const { query, mocks: defaultMocks } = setupSimpleCase(); + + const user = userEvent.setup(); + + const mocks = [ + defaultMocks[0], + { + request: { query }, + result: { data: { greeting: "Hello again" } }, + delay: 20, + }, + { + request: { query }, + result: { data: { greeting: "You again?" } }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + useBackgroundQueryIsPending: false, + usePreloadedQueryHandlersIsPending: false, + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryReference; + }) { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const { refetch } = useQueryRefHandlers(queryRef); + + Profiler.mergeSnapshot({ + usePreloadedQueryHandlersIsPending: isPending, + result: useReadQuery(queryRef), + }); + + return ( + + ); + } + + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const [queryRef, { refetch }] = useBackgroundQuery(query); + + Profiler.mergeSnapshot({ useBackgroundQueryIsPending: isPending }); + + return ( + <> + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch from parent"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: true, + usePreloadedQueryHandlersIsPending: false, + result: { + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + usePreloadedQueryHandlersIsPending: false, + result: { + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await act(() => user.click(screen.getByText("Refetch from child"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + usePreloadedQueryHandlersIsPending: true, + result: { + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + usePreloadedQueryHandlersIsPending: false, + result: { + data: { greeting: "You again?" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("refetches from queryRefs produced by useBackgroundQuery", async () => { + const { query, mocks: defaultMocks } = setupSimpleCase(); + + const user = userEvent.setup(); + + const mocks = [ + defaultMocks[0], + { + request: { query }, + result: { data: { greeting: "Hello again" } }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryReference; + }) { + const { refetch } = useQueryRefHandlers(queryRef); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return ; + } + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); + + return ( + <> + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("refetches from queryRefs produced by useLoadableQuery", async () => { + const { query, mocks: defaultMocks } = setupSimpleCase(); + + const user = userEvent.setup(); + + const mocks = [ + defaultMocks[0], + { + request: { query }, + result: { data: { greeting: "Hello again" } }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryReference; + }) { + const { refetch } = useQueryRefHandlers(queryRef); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return ; + } + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("resuspends when calling `fetchMore`", async () => { + const { query, link } = setupPaginatedCase(); + + const user = userEvent.setup(); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { fetchMore } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + + + + ); + } + + const queryRef = preloadQuery(query); + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Load next"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("properly uses `updateQuery` when calling `fetchMore`", async () => { + const { query, link } = setupPaginatedCase(); + + const user = userEvent.setup(); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { fetchMore } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + + + + ); + } + + const queryRef = preloadQuery(query); + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Load next"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + const { query, link } = setupPaginatedCase(); + + const user = userEvent.setup(); + + const client = new ApolloClient({ + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + letters: concatPagination(), + }, + }, + }, + }), + link, + }); + const preloadQuery = createQueryPreloader(client); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { fetchMore } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + + + + ); + } + + const queryRef = preloadQuery(query); + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Load next"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("paginates from queryRefs produced by useBackgroundQuery", async () => { + const { query, link } = setupPaginatedCase(); + + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryReference; + }) { + useTrackRenders(); + const { fetchMore } = useQueryRefHandlers(queryRef); + + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return ( + + ); + } + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Load next"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("paginates from queryRefs produced by useLoadableQuery", async () => { + const { query, link } = setupPaginatedCase(); + + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryReference; + }) { + useTrackRenders(); + const { fetchMore } = useQueryRefHandlers(queryRef); + + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return ( + + ); + } + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Load next"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("`fetchMore` works with startTransition", async () => { + const { query, link } = setupPaginatedCase(); + + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const { fetchMore } = useQueryRefHandlers(queryRef); + + Profiler.mergeSnapshot({ isPending }); + + return ( + <> + + }> + + + + ); + } + + const queryRef = preloadQuery(query); + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Load next"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: true, + result: { + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("`fetchMore` works with startTransition from useBackgroundQuery and useQueryRefHandlers", async () => { + const { query, link } = setupPaginatedCase(); + + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + const Profiler = createProfiler({ + initialSnapshot: { + useBackgroundQueryIsPending: false, + useQueryRefHandlersIsPending: false, + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryReference; + }) { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const { fetchMore } = useQueryRefHandlers(queryRef); + + Profiler.mergeSnapshot({ + useQueryRefHandlersIsPending: isPending, + result: useReadQuery(queryRef), + }); + + return ( + + ); + } + + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query); + + Profiler.mergeSnapshot({ useBackgroundQueryIsPending: isPending }); + + return ( + <> + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Paginate from parent"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: true, + useQueryRefHandlersIsPending: false, + result: { + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + useQueryRefHandlersIsPending: false, + result: { + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await act(() => user.click(screen.getByText("Paginate from child"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + useQueryRefHandlersIsPending: true, + result: { + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + useQueryRefHandlersIsPending: false, + result: { + data: { + letters: [ + { letter: "E", position: 5 }, + { letter: "F", position: 6 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender(); +}); diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 8a725261f40..78fc82c61f4 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -16,6 +16,8 @@ export type { UseLoadableQueryResult, } from "./useLoadableQuery.js"; export { useLoadableQuery } from "./useLoadableQuery.js"; +export type { UseQueryRefHandlersResult } from "./useQueryRefHandlers.js"; +export { useQueryRefHandlers } from "./useQueryRefHandlers.js"; export type { UseReadQueryResult } from "./useReadQuery.js"; export { useReadQuery } from "./useReadQuery.js"; export { skipToken } from "./constants.js"; diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 47484ad45b8..96ae008360e 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -51,7 +51,8 @@ export function useBackgroundQuery< DeepPartial | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial - : TData + : TData, + TVariables > | (TOptions["skip"] extends boolean ? undefined : never) ), @@ -68,7 +69,7 @@ export function useBackgroundQuery< errorPolicy: "ignore" | "all"; } ): [ - QueryReference | undefined>, + QueryReference | undefined, TVariables>, UseBackgroundQueryResult, ]; @@ -81,7 +82,7 @@ export function useBackgroundQuery< errorPolicy: "ignore" | "all"; } ): [ - QueryReference, + QueryReference, UseBackgroundQueryResult, ]; @@ -95,7 +96,7 @@ export function useBackgroundQuery< returnPartialData: true; } ): [ - QueryReference> | undefined, + QueryReference, TVariables> | undefined, UseBackgroundQueryResult, ]; @@ -108,7 +109,7 @@ export function useBackgroundQuery< returnPartialData: true; } ): [ - QueryReference>, + QueryReference, TVariables>, UseBackgroundQueryResult, ]; @@ -121,7 +122,7 @@ export function useBackgroundQuery< skip: boolean; } ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ]; @@ -131,7 +132,10 @@ export function useBackgroundQuery< >( query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer -): [QueryReference, UseBackgroundQueryResult]; +): [ + QueryReference, + UseBackgroundQueryResult, +]; export function useBackgroundQuery< TData = unknown, @@ -152,7 +156,7 @@ export function useBackgroundQuery< returnPartialData: true; }) ): [ - QueryReference> | undefined, + QueryReference, TVariables> | undefined, UseBackgroundQueryResult, ]; @@ -163,7 +167,7 @@ export function useBackgroundQuery< query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ]; @@ -177,7 +181,7 @@ export function useBackgroundQuery< Partial>) | BackgroundQueryHookOptionsNoInfer = Object.create(null) ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ] { const client = useApolloClient(options.client); @@ -208,7 +212,7 @@ export function useBackgroundQuery< const [wrappedQueryRef, setWrappedQueryRef] = React.useState( wrapQueryRef(queryRef) ); - if (unwrapQueryRef(wrappedQueryRef)[0] !== queryRef) { + if (unwrapQueryRef(wrappedQueryRef) !== queryRef) { setWrappedQueryRef(wrapQueryRef(queryRef)); } if (queryRef.didChangeOptions(watchQueryOptions)) { diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 282988d8a16..96f8cc974fa 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -43,7 +43,7 @@ export type UseLoadableQueryResult< TVariables extends OperationVariables = OperationVariables, > = [ LoadQueryFunction, - QueryReference | null, + QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -119,11 +119,12 @@ export function useLoadableQuery< const watchQueryOptions = useWatchQueryOptions({ client, query, options }); const { queryKey = [] } = options; - const [queryRef, setQueryRef] = React.useState | null>( - null - ); + const [queryRef, setQueryRef] = React.useState | null>(null); - const internalQueryRef = queryRef && unwrapQueryRef(queryRef)[0]; + const internalQueryRef = queryRef && unwrapQueryRef(queryRef); if (queryRef && internalQueryRef?.didChangeOptions(watchQueryOptions)) { const promise = internalQueryRef.applyOptions(watchQueryOptions); diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts new file mode 100644 index 00000000000..b7f58da2f23 --- /dev/null +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -0,0 +1,87 @@ +import * as React from "rehackt"; +import { + getWrappedPromise, + unwrapQueryRef, + updateWrappedQueryRef, + wrapQueryRef, +} from "../cache/QueryReference.js"; +import type { QueryReference } from "../cache/QueryReference.js"; +import type { OperationVariables } from "../../core/types.js"; +import type { RefetchFunction, FetchMoreFunction } from "./useSuspenseQuery.js"; +import type { FetchMoreQueryOptions } from "../../core/watchQueryOptions.js"; + +export interface UseQueryRefHandlersResult< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> { + /** {@inheritDoc @apollo/client!ObservableQuery#refetch:member(1)} */ + refetch: RefetchFunction; + /** {@inheritDoc @apollo/client!ObservableQuery#fetchMore:member(1)} */ + fetchMore: FetchMoreFunction; +} + +/** + * A React hook that returns a `refetch` and `fetchMore` function for a given + * `queryRef`. + * + * This is useful to get access to handlers for a `queryRef` that was created by + * `createQueryPreloader` or when the handlers for a `queryRef` produced in + * a different component are inaccessible. + * + * @example + * ```tsx + * const MyComponent({ queryRef }) { + * const { refetch, fetchMore } = useQueryRefHandlers(queryRef) + * + * // ... + * } + * ``` + * + * @param queryRef a `QueryReference` returned from `useBackgroundQuery` or `createQueryPreloader`. + */ +export function useQueryRefHandlers< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + queryRef: QueryReference +): UseQueryRefHandlersResult { + const [previousQueryRef, setPreviousQueryRef] = React.useState(queryRef); + const [wrappedQueryRef, setWrappedQueryRef] = React.useState(queryRef); + const internalQueryRef = unwrapQueryRef(queryRef); + + // To ensure we can support React transitions, this hook needs to manage the + // queryRef state and apply React's state value immediately to the existing + // queryRef since this hook doesn't return the queryRef directly + if (previousQueryRef !== queryRef) { + setPreviousQueryRef(queryRef); + setWrappedQueryRef(queryRef); + } else { + updateWrappedQueryRef(queryRef, getWrappedPromise(wrappedQueryRef)); + } + + const refetch: RefetchFunction = React.useCallback( + (variables) => { + const promise = internalQueryRef.refetch(variables); + + setWrappedQueryRef(wrapQueryRef(internalQueryRef)); + + return promise; + }, + [internalQueryRef] + ); + + const fetchMore: FetchMoreFunction = React.useCallback( + (options) => { + const promise = internalQueryRef.fetchMore( + options as FetchMoreQueryOptions + ); + + setWrappedQueryRef(wrapQueryRef(internalQueryRef)); + + return promise; + }, + [internalQueryRef] + ); + + return { refetch, fetchMore }; +} diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index f2320aa58ea..f71d83b35a9 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -1,5 +1,6 @@ import * as React from "rehackt"; import { + getWrappedPromise, unwrapQueryRef, updateWrappedQueryRef, } from "../cache/QueryReference.js"; @@ -38,11 +39,23 @@ export interface UseReadQueryResult { export function useReadQuery( queryRef: QueryReference ): UseReadQueryResult { - const [internalQueryRef, getPromise] = React.useMemo( + const internalQueryRef = React.useMemo( () => unwrapQueryRef(queryRef), [queryRef] ); + const getPromise = React.useCallback( + () => getWrappedPromise(queryRef), + [queryRef] + ); + + if (internalQueryRef.disposed) { + internalQueryRef.reinitialize(); + updateWrappedQueryRef(queryRef, internalQueryRef.promise); + } + + React.useEffect(() => internalQueryRef.retain(), [internalQueryRef]); + const promise = useSyncExternalStore( React.useCallback( (forceUpdate) => { diff --git a/src/react/index.ts b/src/react/index.ts index 784046d950b..13f1103e41f 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -13,4 +13,11 @@ export * from "./hooks/index.js"; export type { IDocumentDefinition } from "./parser/index.js"; export { DocumentType, operationName, parser } from "./parser/index.js"; +export type { + PreloadQueryOptions, + PreloadQueryFetchPolicy, + PreloadQueryFunction, +} from "./query-preloader/createQueryPreloader.js"; +export { createQueryPreloader } from "./query-preloader/createQueryPreloader.js"; + export * from "./types/types.js"; diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx new file mode 100644 index 00000000000..38878cdfbb5 --- /dev/null +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -0,0 +1,2508 @@ +import React, { Suspense } from "react"; +import { createQueryPreloader } from "../createQueryPreloader"; +import { + ApolloClient, + ApolloError, + ApolloLink, + InMemoryCache, + NetworkStatus, + OperationVariables, + TypedDocumentNode, + gql, +} from "../../../core"; +import { + MockLink, + MockSubscriptionLink, + MockedResponse, + wait, +} from "../../../testing"; +import { expectTypeOf } from "expect-type"; +import { QueryReference, unwrapQueryRef } from "../../cache/QueryReference"; +import { DeepPartial, Observable } from "../../../utilities"; +import { + SimpleCaseData, + createProfiler, + spyOnConsole, + setupSimpleCase, + useTrackRenders, + setupVariablesCase, + renderWithClient, + VariablesCaseData, +} from "../../../testing/internal"; +import { ApolloProvider } from "../../context"; +import { act, render, renderHook, screen } from "@testing-library/react"; +import { UseReadQueryResult, useReadQuery } from "../../hooks"; +import { GraphQLError } from "graphql"; +import { ErrorBoundary } from "react-error-boundary"; +import userEvent from "@testing-library/user-event"; + +function createDefaultClient(mocks: MockedResponse[]) { + return new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); +} + +function renderDefaultTestApp({ + client, + queryRef, +}: { + client: ApolloClient; + queryRef: QueryReference; +}) { + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + error: null as Error | null, + }, + }); + + function ReadQueryHook() { + useTrackRenders({ name: "ReadQueryHook" }); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function SuspenseFallback() { + useTrackRenders({ name: "SuspenseFallback" }); + return

Loading

; + } + + function ErrorFallback({ error }: { error: Error }) { + useTrackRenders({ name: "ErrorFallback" }); + Profiler.mergeSnapshot({ error }); + + return null; + } + + function App() { + useTrackRenders({ name: "App" }); + + return ( + + }> + + + + ); + } + + const utils = render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + function rerender() { + return utils.rerender(); + } + + return { ...utils, rerender, Profiler }; +} + +test("loads a query and suspends when passed to useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("loads a query with variables and suspends when passed to useReadQuery", async () => { + const { query, mocks } = setupVariablesCase(); + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { + variables: { id: "1" }, + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("Auto disposes of the query ref if not retained within the given time", async () => { + jest.useFakeTimers(); + const { query, mocks } = setupSimpleCase(); + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + + // We don't start the dispose timer until the promise is initially resolved + // so we need to wait for it + jest.advanceTimersByTime(20); + await queryRef.toPromise(); + jest.advanceTimersByTime(30_000); + + expect(queryRef).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + jest.useRealTimers(); +}); + +test("Honors configured auto dispose timer on the client", async () => { + jest.useFakeTimers(); + const { query, mocks } = setupSimpleCase(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + defaultOptions: { + react: { + suspense: { + autoDisposeTimeoutMs: 5000, + }, + }, + }, + }); + + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + + // We don't start the dispose timer until the promise is initially resolved + // so we need to wait for it + jest.advanceTimersByTime(20); + await queryRef.toPromise(); + jest.advanceTimersByTime(5_000); + + expect(queryRef).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + jest.useRealTimers(); +}); + +test("useReadQuery auto-retains the queryRef and disposes of it when unmounted", async () => { + jest.useFakeTimers(); + const { query, mocks } = setupSimpleCase(); + + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + + const { unmount } = renderHook(() => useReadQuery(queryRef)); + + // We don't start the dispose timer until the promise is initially resolved + // so we need to wait for it + jest.advanceTimersByTime(20); + await act(() => queryRef.toPromise()); + jest.advanceTimersByTime(30_000); + + expect(queryRef).not.toBeDisposed(); + + jest.useRealTimers(); + + unmount(); + + await wait(0); + + expect(queryRef).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +test("useReadQuery auto-resubscribes the query after its disposed", async () => { + const { query } = setupSimpleCase(); + + let fetchCount = 0; + const link = new ApolloLink((operation) => { + let count = ++fetchCount; + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { greeting: `Hello ${count}` } }); + observer.complete(); + }, 100); + }); + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + + return ( + <> + + }> + {show && } + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await wait(0); + await Profiler.takeRender(); + + expect(queryRef).toBeDisposed(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // Ensure we aren't refetching the data by checking we still render the same + // cache result + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + client.writeQuery({ query, data: { greeting: "Hello (cached)" } }); + + // Ensure we can get cache updates again after remounting + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello (cached)" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Write a cache result to ensure that remounting will read this result + // instead of the old one + client.writeQuery({ query, data: { greeting: "While you were away" } }); + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(queryRef).not.toBeDisposed(); + + // Ensure we read the newest cache result changed while this queryRef was + // disposed + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "While you were away" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Remove cached data to ensure remounting will refetch the data + client.cache.modify({ + fields: { + greeting: (_, { DELETE }) => DELETE, + }, + }); + + // we wait a moment to ensure no network request is triggered + // by the `cache.modify` (even with a slight delay) + await wait(10); + expect(fetchCount).toBe(1); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // this should now trigger a network request + expect(fetchCount).toBe(2); + expect(queryRef).not.toBeDisposed(); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 2" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("useReadQuery handles auto-resubscribe with returnPartialData", async () => { + const { query, mocks } = setupVariablesCase(); + + let fetchCount = 0; + const link = new ApolloLink((operation) => { + fetchCount++; + const mock = mocks.find( + (mock) => mock.request.variables?.id === operation.variables.id + ); + + if (!mock) { + throw new Error("Could not find mock for variables"); + } + + const result = mock.result as Record; + + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: result.data }); + observer.complete(); + }, 100); + }); + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult> | null, + }, + }); + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { + returnPartialData: true, + variables: { id: "1" }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + + return ( + <> + + }> + {show && } + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await wait(0); + await Profiler.takeRender(); + + expect(queryRef).toBeDisposed(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // Ensure we aren't refetching the data by checking we still render the same + // cache result + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + client.writeQuery({ + query, + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (cached)", + }, + }, + variables: { id: "1" }, + }); + + // Ensure we can get cache updates again after remounting + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (cached)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Write a cache result to ensure that remounting will read this result + // instead of the old one + client.writeQuery({ + query, + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (Away)", + }, + }, + variables: { id: "1" }, + }); + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(queryRef).not.toBeDisposed(); + + // Ensure we read the newest cache result changed while this queryRef was + // disposed + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (Away)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Remove cached data to ensure remounting will refetch the data + client.cache.modify({ + id: "Character:1", + fields: { + name: (_, { DELETE }) => DELETE, + }, + }); + + // we wait a moment to ensure no network request is triggered + // by the `cache.modify` (even with a slight delay) + await wait(10); + expect(fetchCount).toBe(1); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // this should now trigger a network request + expect(fetchCount).toBe(2); + expect(queryRef).not.toBeDisposed(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + // Ensure that remounting without data in the cache will fetch and suspend + client.clearStore(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(fetchCount).toBe(3); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("useReadQuery handles auto-resubscribe on network-only fetch policy", async () => { + const { query } = setupSimpleCase(); + + let fetchCount = 0; + const link = new ApolloLink((operation) => { + let count = ++fetchCount; + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { greeting: `Hello ${count}` } }); + observer.complete(); + }, 10); + }); + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { fetchPolicy: "network-only" }); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + + return ( + <> + + }> + {show && } + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await wait(0); + await Profiler.takeRender(); + + expect(queryRef).toBeDisposed(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // Ensure we aren't refetching the data by checking we still render the same + // cache result + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + client.writeQuery({ query, data: { greeting: "Hello (cached)" } }); + + // Ensure we can get cache updates again after remounting + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello (cached)" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Write a cache result to ensure that remounting will read this result + // instead of the old one + client.writeQuery({ query, data: { greeting: "While you were away" } }); + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(queryRef).not.toBeDisposed(); + + // Ensure we read the newest cache result changed while this queryRef was + // disposed + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "While you were away" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Remove cached data to ensure remounting will refetch the data + client.cache.modify({ + fields: { + greeting: (_, { DELETE }) => DELETE, + }, + }); + + // Ensure the delete doesn't immediately fetch + await wait(10); + expect(fetchCount).toBe(1); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(fetchCount).toBe(2); + expect(queryRef).not.toBeDisposed(); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 2" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("useReadQuery handles auto-resubscribe on cache-and-network fetch policy", async () => { + const { query } = setupSimpleCase(); + + let fetchCount = 0; + const link = new ApolloLink((operation) => { + let count = ++fetchCount; + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { greeting: `Hello ${count}` } }); + observer.complete(); + }, 10); + }); + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { fetchPolicy: "cache-and-network" }); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + + return ( + <> + + }> + {show && } + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await wait(0); + await Profiler.takeRender(); + + expect(queryRef).toBeDisposed(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // Ensure we aren't refetching the data by checking we still render the same + // cache result + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + client.writeQuery({ query, data: { greeting: "Hello (cached)" } }); + + // Ensure we can get cache updates again after remounting + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello (cached)" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Write a cache result to ensure that remounting will read this result + // instead of the old one + client.writeQuery({ query, data: { greeting: "While you were away" } }); + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(queryRef).not.toBeDisposed(); + + // Ensure we read the newest cache result changed while this queryRef was + // disposed + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "While you were away" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Remove cached data to ensure remounting will refetch the data + client.cache.modify({ + fields: { + greeting: (_, { DELETE }) => DELETE, + }, + }); + + // Ensure delete doesn't refetch immediately + await wait(10); + expect(fetchCount).toBe(1); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(fetchCount).toBe(2); + expect(queryRef).not.toBeDisposed(); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 2" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("useReadQuery handles auto-resubscribe on no-cache fetch policy", async () => { + const { query } = setupSimpleCase(); + + let fetchCount = 0; + const link = new ApolloLink((operation) => { + let count = ++fetchCount; + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { greeting: `Hello ${count}` } }); + observer.complete(); + }, 10); + }); + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { fetchPolicy: "no-cache" }); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + + return ( + <> + + }> + {show && } + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await wait(0); + await Profiler.takeRender(); + + expect(queryRef).toBeDisposed(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // Ensure we aren't refetching the data by checking we still render the same + // result + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + // Ensure caches writes for the query are ignored by the hook + client.writeQuery({ query, data: { greeting: "Hello (cached)" } }); + + await expect(Profiler).not.toRerender(); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Write a cache result to ensure that remounting will ignore this result + client.writeQuery({ query, data: { greeting: "While you were away" } }); + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(queryRef).not.toBeDisposed(); + + // Ensure we continue to read the same value + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Remove cached data to verify this type of cache change is also ignored + client.cache.modify({ + fields: { + greeting: (_, { DELETE }) => DELETE, + }, + }); + + // Ensure delete doesn't fire off request + await wait(10); + expect(fetchCount).toBe(1); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + // Ensure we are still rendering the same result and haven't refetched + // anything based on missing cache data + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("reacts to cache updates", async () => { + const { query, mocks } = setupSimpleCase(); + const client = createDefaultClient(mocks); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ + query, + data: { greeting: "Hello (updated)" }, + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello (updated)" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("ignores cached result and suspends when `fetchPolicy` is network-only", async () => { + const { query, mocks } = setupSimpleCase(); + + const client = createDefaultClient(mocks); + client.writeQuery({ query, data: { greeting: "Cached Hello" } }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { + fetchPolicy: "network-only", + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("does not cache results when `fetchPolicy` is no-cache", async () => { + const { query, mocks } = setupSimpleCase(); + + const client = createDefaultClient(mocks); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { + fetchPolicy: "no-cache", + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(client.extract()).toEqual({}); +}); + +test("returns initial cache data followed by network data when `fetchPolicy` is cache-and-network", async () => { + const { query, mocks } = setupSimpleCase(); + + const client = createDefaultClient(mocks); + client.writeQuery({ query, data: { greeting: "Cached Hello" } }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { + fetchPolicy: "cache-and-network", + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { greeting: "Cached Hello" }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("returns cached data when all data is present in the cache", async () => { + const { query, mocks } = setupSimpleCase(); + + const client = createDefaultClient(mocks); + client.writeQuery({ query, data: { greeting: "Cached Hello" } }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { greeting: "Cached Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("suspends and ignores partial data in the cache", async () => { + const query = gql` + query { + hello + foo + } + `; + + const mocks = [ + { + request: { query }, + result: { data: { hello: "from link", foo: "bar" } }, + delay: 20, + }, + ]; + + const client = createDefaultClient(mocks); + + { + // we expect a "Missing field 'foo' while writing result..." error + // when writing hello to the cache, so we'll silence it + using _consoleSpy = spyOnConsole("error"); + client.writeQuery({ query, data: { hello: "from cache" } }); + } + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { hello: "from link", foo: "bar" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("throws when error is returned", async () => { + // Disable error messages shown by React when an error is thrown to an error + // boundary + using _consoleSpy = spyOnConsole("error"); + const { query } = setupSimpleCase(); + const mocks = [ + { request: { query }, result: { errors: [new GraphQLError("Oops")] } }, + ]; + const client = createDefaultClient(mocks); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ErrorFallback"]); + expect(snapshot.error).toEqual( + new ApolloError({ graphQLErrors: [new GraphQLError("Oops")] }) + ); + } +}); + +test("returns error when error policy is 'all'", async () => { + // Disable error messages shown by React when an error is thrown to an error + // boundary + using _consoleSpy = spyOnConsole("error"); + const { query } = setupSimpleCase(); + const mocks = [ + { request: { query }, result: { errors: [new GraphQLError("Oops")] } }, + ]; + const client = createDefaultClient(mocks); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { errorPolicy: "all" }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: undefined, + error: new ApolloError({ graphQLErrors: [new GraphQLError("Oops")] }), + networkStatus: NetworkStatus.error, + }); + expect(snapshot.error).toEqual(null); + } +}); + +test("discards error when error policy is 'ignore'", async () => { + // Disable error messages shown by React when an error is thrown to an error + // boundary + using _consoleSpy = spyOnConsole("error"); + const { query } = setupSimpleCase(); + const mocks = [ + { request: { query }, result: { errors: [new GraphQLError("Oops")] } }, + ]; + const client = createDefaultClient(mocks); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { errorPolicy: "ignore" }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: undefined, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(snapshot.error).toEqual(null); + } +}); + +test("passes context to the link", async () => { + interface QueryData { + context: Record; + } + + const query: TypedDocumentNode = gql` + query ContextQuery { + context + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink((operation) => { + return new Observable((observer) => { + const { valueA, valueB } = operation.getContext(); + setTimeout(() => { + observer.next({ data: { context: { valueA, valueB } } }); + observer.complete(); + }, 10); + }); + }), + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { + context: { valueA: "A", valueB: "B" }, + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + // initial render + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { context: { valueA: "A", valueB: "B" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); +}); + +test("creates unique query refs when calling preloadQuery with the same query", async () => { + const { query } = setupSimpleCase(); + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + maxUsageCount: Infinity, + }, + ]; + + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const queryRef1 = preloadQuery(query); + const queryRef2 = preloadQuery(query); + + const unwrappedQueryRef1 = unwrapQueryRef(queryRef1); + const unwrappedQueryRef2 = unwrapQueryRef(queryRef2); + + // Use Object.is inside expect to prevent circular reference errors on toBe + expect(Object.is(queryRef1, queryRef2)).toBe(false); + expect(Object.is(unwrappedQueryRef1, unwrappedQueryRef2)).toBe(false); + + await expect(queryRef1.toPromise()).resolves.toBe(queryRef1); + await expect(queryRef2.toPromise()).resolves.toBe(queryRef2); +}); + +test("does not suspend and returns partial data when `returnPartialData` is `true`", async () => { + const { query, mocks } = setupVariablesCase(); + const partialQuery = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + } + } + `; + + const client = createDefaultClient(mocks); + + client.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { + variables: { id: "1" }, + returnPartialData: true, + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + networkStatus: NetworkStatus.loading, + error: undefined, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } +}); + +test('enables canonical results when canonizeResults is "true"', async () => { + interface Result { + __typename: string; + value: number; + } + + interface QueryData { + results: Result[]; + } + + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); + + const query: TypedDocumentNode = gql` + query { + results { + value + } + } + `; + + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; + + cache.writeQuery({ + query, + data: { results }, + }); + + const client = new ApolloClient({ cache, link: new MockLink([]) }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { canonizeResults: true }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + const { snapshot } = await Profiler.takeRender(); + const resultSet = new Set(snapshot.result?.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(snapshot.result).toEqual({ + data: { results }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); +}); + +test("can disable canonical results when the cache's canonizeResults setting is true", async () => { + interface Result { + __typename: string; + value: number; + } + + const cache = new InMemoryCache({ + canonizeResults: true, + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); + + const query: TypedDocumentNode<{ results: Result[] }, never> = gql` + query { + results { + value + } + } + `; + + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; + + cache.writeQuery({ + query, + data: { results }, + }); + + const client = new ApolloClient({ cache, link: new MockLink([]) }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { canonizeResults: false }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + const { snapshot } = await Profiler.takeRender(); + const resultSet = new Set(snapshot.result!.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(snapshot.result).toEqual({ + data: { results }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); +}); + +test("suspends deferred queries until initial chunk loads then rerenders with deferred data", async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + link.simulateResult({ + result: { + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + hasNext: true, + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +describe.skip("type tests", () => { + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink([]), + }); + const preloadQuery = createQueryPreloader(client); + + test("variables are optional and can be anything with untyped DocumentNode", () => { + const query = gql``; + + preloadQuery(query); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, { variables: { foo: "bar" } }); + preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); + }); + + test("variables are optional and can be anything with unspecified TVariables", () => { + type Data = { greeting: string }; + const query: TypedDocumentNode = gql``; + + preloadQuery(query); + preloadQuery(query); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, { variables: { foo: "bar" } }); + preloadQuery(query, { variables: { foo: "bar" } }); + preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); + preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); + }); + + test("variables are optional when TVariables are empty", () => { + type Data = { greeting: string }; + type Variables = Record; + const query: TypedDocumentNode = gql``; + + preloadQuery(query); + preloadQuery(query); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, { + returnPartialData: true, + variables: {}, + }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variables + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variables + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variables + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variables + foo: "bar", + }, + }); + }); + + test("does not allow variables when TVariables is `never`", () => { + type Data = { greeting: string }; + const query: TypedDocumentNode = gql``; + + preloadQuery(query); + preloadQuery(query); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, { + returnPartialData: true, + variables: {}, + }); + // @ts-expect-error no variables allowed + preloadQuery(query, { variables: { foo: "bar" } }); + // @ts-expect-error no variables allowed + preloadQuery(query, { variables: { foo: "bar" } }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error no variables allowed + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error no variables allowed + foo: "bar", + }, + }); + }); + + test("optional variables are optional", () => { + type Data = { posts: string[] }; + type Variables = { limit?: number }; + const query: TypedDocumentNode = gql``; + + preloadQuery(query); + preloadQuery(query); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, { + returnPartialData: true, + variables: {}, + }); + preloadQuery(query, { variables: { limit: 10 } }); + preloadQuery(query, { variables: { limit: 10 } }); + preloadQuery(query, { returnPartialData: true, variables: { limit: 10 } }); + preloadQuery(query, { + returnPartialData: true, + variables: { limit: 10 }, + }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + test("enforces required variables", () => { + type Data = { character: string }; + type Variables = { id: string }; + const query: TypedDocumentNode = gql``; + + // @ts-expect-error missing variables option + preloadQuery(query); + // @ts-expect-error missing variables option + preloadQuery(query); + // @ts-expect-error missing variables option + preloadQuery(query, { returnPartialData: true }); + // @ts-expect-error missing variables option + preloadQuery(query, { returnPartialData: true }); + preloadQuery(query, { + // @ts-expect-error empty variables + variables: {}, + }); + preloadQuery(query, { + // @ts-expect-error empty variables + variables: {}, + }); + preloadQuery(query, { + returnPartialData: true, + // @ts-expect-error empty variables + variables: {}, + }); + preloadQuery(query, { + returnPartialData: true, + // @ts-expect-error empty variables + variables: {}, + }); + preloadQuery(query, { variables: { id: "1" } }); + preloadQuery(query, { variables: { id: "1" } }); + preloadQuery(query, { returnPartialData: true, variables: { id: "1" } }); + preloadQuery(query, { + returnPartialData: true, + variables: { id: "1" }, + }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + test("requires variables with mixed TVariables", () => { + type Data = { character: string }; + type Variables = { id: string; language?: string }; + const query: TypedDocumentNode = gql``; + + // @ts-expect-error missing variables argument + preloadQuery(query); + // @ts-expect-error missing variables argument + preloadQuery(query); + // @ts-expect-error missing variables argument + preloadQuery(query, {}); + // @ts-expect-error missing variables argument + preloadQuery(query, {}); + // @ts-expect-error missing variables option + preloadQuery(query, { returnPartialData: true }); + // @ts-expect-error missing variables option + preloadQuery(query, { returnPartialData: true }); + preloadQuery(query, { + // @ts-expect-error missing required variables + variables: {}, + }); + preloadQuery(query, { + // @ts-expect-error missing required variables + variables: {}, + }); + preloadQuery(query, { + returnPartialData: true, + // @ts-expect-error missing required variables + variables: {}, + }); + preloadQuery(query, { + returnPartialData: true, + // @ts-expect-error missing required variables + variables: {}, + }); + preloadQuery(query, { variables: { id: "1" } }); + preloadQuery(query, { variables: { id: "1" } }); + preloadQuery(query, { + // @ts-expect-error missing required variable + variables: { language: "en" }, + }); + preloadQuery(query, { + // @ts-expect-error missing required variable + variables: { language: "en" }, + }); + preloadQuery(query, { variables: { id: "1", language: "en" } }); + preloadQuery(query, { + variables: { id: "1", language: "en" }, + }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + test("returns QueryReference when TData cannot be inferred", () => { + const query = gql``; + + const queryRef = preloadQuery(query); + + expectTypeOf(queryRef).toEqualTypeOf>(); + }); + + test("returns QueryReference in default case", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + }); + + test("returns QueryReference with errorPolicy: 'ignore'", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { errorPolicy: "ignore" }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + errorPolicy: "ignore", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + }); + + test("returns QueryReference with errorPolicy: 'all'", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { errorPolicy: "all" }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + errorPolicy: "all", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + }); + + test("returns QueryReference with errorPolicy: 'none'", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { errorPolicy: "none" }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + }); + + test("returns QueryReference> with returnPartialData: true", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { returnPartialData: true }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, { [key: string]: any }> + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: true, + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, OperationVariables> + >(); + } + }); + + test("returns QueryReference> with returnPartialData: false", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { returnPartialData: false }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: false, + }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + } + }); + + test("returns QueryReference when passing an option unrelated to TData", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { canonizeResults: true }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + canonizeResults: true, + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + }); + + test("handles combinations of options", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference< + DeepPartial | undefined, + { [key: string]: any } + > + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference< + DeepPartial | undefined, + OperationVariables + > + >(); + } + + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, { [key: string]: any }> + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, OperationVariables> + >(); + } + }); + + test("returns correct TData type when combined with options unrelated to TData", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { + canonizeResults: true, + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, { [key: string]: any }> + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + canonizeResults: true, + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, OperationVariables> + >(); + } + }); +}); diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts new file mode 100644 index 00000000000..25a1bdc2858 --- /dev/null +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -0,0 +1,193 @@ +import type { + ApolloClient, + DefaultContext, + DocumentNode, + ErrorPolicy, + OperationVariables, + RefetchWritePolicy, + TypedDocumentNode, + WatchQueryFetchPolicy, + WatchQueryOptions, +} from "../../core/index.js"; +import type { + DeepPartial, + OnlyRequiredProperties, +} from "../../utilities/index.js"; +import { + InternalQueryReference, + wrapQueryRef, +} from "../cache/QueryReference.js"; +import type { QueryReference } from "../cache/QueryReference.js"; +import type { NoInfer } from "../index.js"; + +type VariablesOption = + [TVariables] extends [never] ? + { + /** {@inheritDoc @apollo/client!QueryOptions#variables:member} */ + variables?: Record; + } + : {} extends OnlyRequiredProperties ? + { + /** {@inheritDoc @apollo/client!QueryOptions#variables:member} */ + variables?: TVariables; + } + : { + /** {@inheritDoc @apollo/client!QueryOptions#variables:member} */ + variables: TVariables; + }; + +export type PreloadQueryFetchPolicy = Extract< + WatchQueryFetchPolicy, + "cache-first" | "network-only" | "no-cache" | "cache-and-network" +>; + +export type PreloadQueryOptions< + TVariables extends OperationVariables = OperationVariables, +> = { + /** {@inheritDoc @apollo/client!QueryOptions#canonizeResults:member} */ + canonizeResults?: boolean; + /** {@inheritDoc @apollo/client!QueryOptions#context:member} */ + context?: DefaultContext; + /** {@inheritDoc @apollo/client!QueryOptions#errorPolicy:member} */ + errorPolicy?: ErrorPolicy; + /** {@inheritDoc @apollo/client!QueryOptions#fetchPolicy:member} */ + fetchPolicy?: PreloadQueryFetchPolicy; + /** {@inheritDoc @apollo/client!QueryOptions#returnPartialData:member} */ + returnPartialData?: boolean; + /** {@inheritDoc @apollo/client!WatchQueryOptions#refetchWritePolicy:member} */ + refetchWritePolicy?: RefetchWritePolicy; +} & VariablesOption; + +type PreloadQueryOptionsArg< + TVariables extends OperationVariables, + TOptions = unknown, +> = [TVariables] extends [never] ? + [options?: PreloadQueryOptions & TOptions] +: {} extends OnlyRequiredProperties ? + [ + options?: PreloadQueryOptions> & + Omit, + ] +: [ + options: PreloadQueryOptions> & + Omit, + ]; + +/** + * A function that will begin loading a query when called. It's result can be + * read by {@link useReadQuery} which will suspend until the query is loaded. + * This is useful when you want to start loading a query as early as possible + * outside of a React component. + * + * @example + * ```js + * const preloadQuery = createQueryPreloader(client); + * const queryRef = preloadQuery(query, { variables, ...otherOptions }); + * + * function App() { + * return ( + * Loading}> + * + * + * ); + * } + * + * function MyQuery() { + * const { data } = useReadQuery(queryRef); + * + * // do something with `data` + * } + * ``` + */ +export interface PreloadQueryFunction { + /** {@inheritDoc @apollo/client!PreloadQueryFunction:interface} */ + < + TData, + TVariables extends OperationVariables, + TOptions extends Omit, + >( + query: DocumentNode | TypedDocumentNode, + ...[options]: PreloadQueryOptionsArg, TOptions> + ): QueryReference< + TOptions["errorPolicy"] extends "ignore" | "all" ? + TOptions["returnPartialData"] extends true ? + DeepPartial | undefined + : TData | undefined + : TOptions["returnPartialData"] extends true ? DeepPartial + : TData, + TVariables + >; + + /** {@inheritDoc @apollo/client!PreloadQueryFunction:interface} */ + ( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + } + ): QueryReference | undefined, TVariables>; + + /** {@inheritDoc @apollo/client!PreloadQueryFunction:interface} */ + ( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & { + errorPolicy: "ignore" | "all"; + } + ): QueryReference; + + /** {@inheritDoc @apollo/client!PreloadQueryFunction:interface} */ + ( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & { + returnPartialData: true; + } + ): QueryReference, TVariables>; + + /** {@inheritDoc @apollo/client!PreloadQueryFunction:interface} */ + ( + query: DocumentNode | TypedDocumentNode, + ...[options]: PreloadQueryOptionsArg> + ): QueryReference; +} + +/** + * A higher order function that returns a `preloadQuery` function which + * can be used to begin loading a query with the given `client`. This is useful + * when you want to start loading a query as early as possible outside of a + * React component. + * + * @param client - The ApolloClient instance that will be used to load queries + * from the returned `preloadQuery` function. + * @returns The `preloadQuery` function. + * + * @example + * ```js + * const preloadQuery = createQueryPreloader(client); + * ``` + * @experimental + */ +export function createQueryPreloader( + client: ApolloClient +): PreloadQueryFunction { + return function preloadQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, + >( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & + VariablesOption = Object.create(null) + ): QueryReference { + const queryRef = new InternalQueryReference( + client.watchQuery({ + ...options, + query, + } as WatchQueryOptions), + { + autoDisposeTimeoutMs: + client.defaultOptions.react?.suspense?.autoDisposeTimeoutMs, + } + ); + + return wrapQueryRef(queryRef); + }; +} diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index 73a9a00ff0e..9f8b3faae9a 100644 --- a/src/testing/internal/index.ts +++ b/src/testing/internal/index.ts @@ -1,3 +1,22 @@ export * from "./profile/index.js"; export * from "./disposables/index.js"; export { ObservableStream } from "./ObservableStream.js"; + +export type { + SimpleCaseData, + PaginatedCaseData, + PaginatedCaseVariables, + VariablesCaseData, + VariablesCaseVariables, +} from "./scenarios/index.js"; +export { + setupSimpleCase, + setupVariablesCase, + setupPaginatedCase, +} from "./scenarios/index.js"; + +export type { + RenderWithClientOptions, + RenderWithMocksOptions, +} from "./renderHelpers.js"; +export { renderWithClient, renderWithMocks } from "./renderHelpers.js"; diff --git a/src/testing/internal/renderHelpers.tsx b/src/testing/internal/renderHelpers.tsx new file mode 100644 index 00000000000..c47a533d09c --- /dev/null +++ b/src/testing/internal/renderHelpers.tsx @@ -0,0 +1,70 @@ +import * as React from "react"; +import type { ReactElement } from "react"; +import { render } from "@testing-library/react"; +import type { Queries, RenderOptions, queries } from "@testing-library/react"; +import type { ApolloClient } from "../../core/index.js"; +import { ApolloProvider } from "../../react/index.js"; +import type { MockedProviderProps } from "../react/MockedProvider.js"; +import { MockedProvider } from "../react/MockedProvider.js"; + +export interface RenderWithClientOptions< + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +> extends RenderOptions { + client: ApolloClient; +} + +export function renderWithClient< + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +>( + ui: ReactElement, + { + client, + wrapper: Wrapper = React.Fragment, + ...renderOptions + }: RenderWithClientOptions +) { + return render(ui, { + ...renderOptions, + wrapper: ({ children }) => { + return ( + + {children} + + ); + }, + }); +} + +export interface RenderWithMocksOptions< + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +> extends RenderOptions, + MockedProviderProps {} + +export function renderWithMocks< + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +>( + ui: ReactElement, + { + wrapper: Wrapper = React.Fragment, + ...renderOptions + }: RenderWithMocksOptions +) { + return render(ui, { + ...renderOptions, + wrapper: ({ children }) => { + return ( + + {children} + + ); + }, + }); +} diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts new file mode 100644 index 00000000000..411099d3615 --- /dev/null +++ b/src/testing/internal/scenarios/index.ts @@ -0,0 +1,108 @@ +import { ApolloLink, Observable, gql } from "../../../core/index.js"; +import type { TypedDocumentNode } from "../../../core/index.js"; +import type { MockedResponse } from "../../core/index.js"; + +export interface SimpleCaseData { + greeting: string; +} + +export function setupSimpleCase() { + const query: TypedDocumentNode> = gql` + query GreetingQuery { + greeting + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + delay: 10, + }, + ]; + + return { query, mocks }; +} + +export interface VariablesCaseData { + character: { + __typename: "Character"; + id: string; + name: string; + }; +} + +export interface VariablesCaseVariables { + id: string; +} + +export function setupVariablesCase() { + const query: TypedDocumentNode = + gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"]; + + const mocks: MockedResponse[] = [...CHARACTERS].map( + (name, index) => ({ + request: { query, variables: { id: String(index + 1) } }, + result: { + data: { + character: { __typename: "Character", id: String(index + 1), name }, + }, + }, + delay: 20, + }) + ); + + return { mocks, query }; +} + +interface Letter { + letter: string; + position: number; +} + +export interface PaginatedCaseData { + letters: Letter[]; +} + +export interface PaginatedCaseVariables { + limit?: number; + offset?: number; +} + +export function setupPaginatedCase() { + const query: TypedDocumentNode = + gql` + query letters($limit: Int, $offset: Int) { + letters(limit: $limit) { + letter + position + } + } + `; + + const data = "ABCDEFGHIJKLMNOPQRSTUV" + .split("") + .map((letter, index) => ({ letter, position: index + 1 })); + + const link = new ApolloLink((operation) => { + const { offset = 0, limit = 2 } = operation.variables; + const letters = data.slice(offset, offset + limit); + + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { letters } }); + observer.complete(); + }, 10); + }); + }); + + return { query, link }; +} diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 690589af128..b73b33cfd08 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -3,6 +3,7 @@ import type { DocumentNode, OperationVariables, } from "../../core/index.js"; +import type { QueryReference } from "../../react/index.js"; import { NextRenderOptions, Profiler, @@ -11,6 +12,11 @@ import { } from "../internal/index.js"; interface ApolloCustomMatchers { + /** + * Used to determine if a queryRef has been disposed. + */ + toBeDisposed: T extends QueryReference ? () => R + : { error: "matcher needs to be called on a QueryReference" }; /** * Used to determine if two GraphQL query documents are equal to each other by * comparing their printed values. The document must be parsed by `gql`. diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index 709bfbad53b..c4f88544f14 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -3,8 +3,10 @@ import { toMatchDocument } from "./toMatchDocument.js"; import { toHaveSuspenseCacheEntryUsing } from "./toHaveSuspenseCacheEntryUsing.js"; import { toRerender, toRenderExactlyTimes } from "./ProfiledComponent.js"; import { toBeGarbageCollected } from "./toBeGarbageCollected.js"; +import { toBeDisposed } from "./toBeDisposed.js"; expect.extend({ + toBeDisposed, toHaveSuspenseCacheEntryUsing, toMatchDocument, toRerender, diff --git a/src/testing/matchers/toBeDisposed.ts b/src/testing/matchers/toBeDisposed.ts new file mode 100644 index 00000000000..452cd45ef6b --- /dev/null +++ b/src/testing/matchers/toBeDisposed.ts @@ -0,0 +1,35 @@ +import type { MatcherFunction } from "expect"; +import type { QueryReference } from "../../react/cache/QueryReference.js"; +import { + InternalQueryReference, + unwrapQueryRef, +} from "../../react/cache/QueryReference.js"; + +function isQueryRef(queryRef: unknown): queryRef is QueryReference { + try { + return unwrapQueryRef(queryRef as any) instanceof InternalQueryReference; + } catch (e) { + return false; + } +} + +export const toBeDisposed: MatcherFunction<[]> = function (queryRef) { + const hint = this.utils.matcherHint("toBeDisposed", "queryRef", "", { + isNot: this.isNot, + }); + + if (!isQueryRef(queryRef)) { + throw new Error(`\n${hint}\n\nmust be called with a valid QueryReference`); + } + + const pass = unwrapQueryRef(queryRef).disposed; + + return { + pass, + message: () => { + return `${hint}\n\nExpected queryRef ${ + this.isNot ? "not " : "" + }to be disposed, but it was${this.isNot ? "" : " not"}.`; + }, + }; +}; From 5cce53e83b976f85d2d2b06e28cc38f01324fea1 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 18 Dec 2023 18:17:53 +0100 Subject: [PATCH 61/90] deprecate `canonizeResults` (#11435) * deprecate `canonizeResults` --- .api-reports/api-report-cache.md | 8 +++++--- .api-reports/api-report-core.md | 14 +++++++++----- .api-reports/api-report-react.md | 13 +++++++++---- .api-reports/api-report-react_components.md | 12 ++++++++---- .api-reports/api-report-react_context.md | 12 ++++++++---- .api-reports/api-report-react_hoc.md | 12 ++++++++---- .api-reports/api-report-react_hooks.md | 13 +++++++++---- .api-reports/api-report-react_ssr.md | 12 ++++++++---- .api-reports/api-report-testing.md | 12 ++++++++---- .api-reports/api-report-testing_core.md | 12 ++++++++---- .api-reports/api-report-utilities.md | 16 ++++++++++------ .api-reports/api-report.md | 15 ++++++++++----- .changeset/tough-timers-begin.md | 8 ++++++++ docs/shared/useBackgroundQuery-options.mdx | 4 ++++ docs/shared/useFragment-options.mdx | 4 ++++ docs/shared/useSuspenseQuery-options.mdx | 4 ++++ docs/source/api/cache/InMemoryCache.mdx | 8 ++++++++ docs/source/api/react/hooks.mdx | 1 - docs/source/caching/garbage-collection.mdx | 4 ++++ src/cache/core/types/Cache.ts | 7 +++++++ src/cache/core/types/DataProxy.ts | 12 ++++++++++++ src/cache/inmemory/types.ts | 13 +++++++++++++ src/core/watchQueryOptions.ts | 6 ++++++ src/react/types/types.ts | 6 ++++++ 24 files changed, 176 insertions(+), 52 deletions(-) create mode 100644 .changeset/tough-timers-begin.md diff --git a/.api-reports/api-report-cache.md b/.api-reports/api-report-cache.md index 8cd3cb53be1..55957897f33 100644 --- a/.api-reports/api-report-cache.md +++ b/.api-reports/api-report-cache.md @@ -129,7 +129,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -234,12 +234,14 @@ export namespace DataProxy { } // (undocumented) export interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; } // (undocumented) export interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -580,7 +582,7 @@ export class InMemoryCache extends ApolloCache { // @public (undocumented) export interface InMemoryCacheConfig extends ApolloReducerConfig { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) fragments?: FragmentRegistryAPI; @@ -971,7 +973,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:92:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 1e0a35ce6ed..04da92c9c0a 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -336,7 +336,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -482,12 +482,14 @@ export namespace DataProxy { } // (undocumented) export interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; } // (undocumented) export interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1101,7 +1103,7 @@ export class InMemoryCache extends ApolloCache { // @public (undocumented) export interface InMemoryCacheConfig extends ApolloReducerConfig { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // Warning: (ae-forgotten-export) The symbol "FragmentRegistryAPI" needs to be exported by the entry point index.d.ts // @@ -1831,6 +1833,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; errorPolicy?: ErrorPolicy; @@ -2158,6 +2161,7 @@ export type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public export interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; errorPolicy?: ErrorPolicy; @@ -2212,14 +2216,14 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:92:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:261:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:310:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index b9a8d200c9d..5fbb07582ff 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -456,7 +456,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -585,6 +585,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -593,6 +594,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1060,6 +1062,7 @@ export type LoadableQueryHookFetchPolicy = Extract; @@ -1751,6 +1754,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: Context; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -2371,6 +2375,7 @@ type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: Context; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -2394,7 +2399,7 @@ interface WatchQueryOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -526,6 +526,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -534,6 +535,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1445,6 +1447,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1724,6 +1727,7 @@ type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1748,7 +1752,7 @@ interface WatchQueryOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -509,6 +509,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -517,6 +518,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1353,6 +1355,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1620,6 +1623,7 @@ type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1644,7 +1648,7 @@ interface WatchQueryOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -508,6 +508,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -516,6 +517,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1422,6 +1424,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1648,6 +1651,7 @@ type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1689,7 +1693,7 @@ export function withSubscription extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -549,6 +549,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -557,6 +558,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1008,6 +1010,7 @@ type LoadableQueryHookFetchPolicy = Extract; @@ -1625,6 +1628,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -2207,6 +2211,7 @@ type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -2230,7 +2235,7 @@ interface WatchQueryOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -479,6 +479,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -487,6 +488,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1339,6 +1341,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1606,6 +1609,7 @@ type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1630,7 +1634,7 @@ interface WatchQueryOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -473,6 +473,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -481,6 +482,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1424,6 +1426,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1659,6 +1662,7 @@ type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1692,7 +1696,7 @@ export function withWarningSpy(it: (...args: TArgs // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:141:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts @@ -1708,8 +1712,8 @@ export function withWarningSpy(it: (...args: TArgs // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:261:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:310:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 98b95dd615f..341da7c1d35 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -349,7 +349,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -472,6 +472,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -480,6 +481,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1381,6 +1383,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1616,6 +1619,7 @@ type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -1649,7 +1653,7 @@ export function withWarningSpy(it: (...args: TArgs // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:141:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts @@ -1665,8 +1669,8 @@ export function withWarningSpy(it: (...args: TArgs // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:261:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:310:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 5ebbd459f66..1478b419cec 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -405,7 +405,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -609,6 +609,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -617,6 +618,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1351,7 +1353,7 @@ class InMemoryCache extends ApolloCache { // // @public (undocumented) interface InMemoryCacheConfig extends ApolloReducerConfig { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // Warning: (ae-forgotten-export) The symbol "FragmentRegistryAPI" needs to be exported by the entry point index.d.ts // @@ -2186,6 +2188,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -2564,6 +2567,7 @@ type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" @@ -2621,13 +2625,13 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:141:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:57:3 - (ae-forgotten-export) The symbol "TypePolicy" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "FieldReadFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:163:3 - (ae-forgotten-export) The symbol "FieldMergeFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/writeToStore.ts:65:7 - (ae-forgotten-export) The symbol "MergeTree" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts @@ -2639,8 +2643,8 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:261:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:310:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/utilities/graphql/storeUtils.ts:226:12 - (ae-forgotten-export) The symbol "storeKeyNameStringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index acd0311dda6..1fa0667d5e7 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -438,7 +438,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -592,12 +592,14 @@ export namespace DataProxy { } // (undocumented) export interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; } // (undocumented) export interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -1290,7 +1292,7 @@ export class InMemoryCache extends ApolloCache { // @public (undocumented) export interface InMemoryCacheConfig extends ApolloReducerConfig { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // Warning: (ae-forgotten-export) The symbol "FragmentRegistryAPI" needs to be exported by the entry point index.d.ts // @@ -1476,6 +1478,7 @@ export type LoadableQueryHookFetchPolicy = Extract; context?: DefaultContext; @@ -2309,6 +2312,7 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; errorPolicy?: ErrorPolicy; @@ -3017,6 +3021,7 @@ export type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public export interface WatchQueryOptions { + // @deprecated (undocumented) canonizeResults?: boolean; context?: DefaultContext; errorPolicy?: ErrorPolicy; @@ -3071,14 +3076,14 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:92:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:261:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:310:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:31:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts diff --git a/.changeset/tough-timers-begin.md b/.changeset/tough-timers-begin.md new file mode 100644 index 00000000000..53fac70e002 --- /dev/null +++ b/.changeset/tough-timers-begin.md @@ -0,0 +1,8 @@ +--- +"@apollo/client": minor +--- + +Deprecates `canonizeResults`. + +Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +A future version of Apollo Client will contain a similar feature without the risk of memory leaks. diff --git a/docs/shared/useBackgroundQuery-options.mdx b/docs/shared/useBackgroundQuery-options.mdx index eb0157d6fdd..d00042a9c98 100644 --- a/docs/shared/useBackgroundQuery-options.mdx +++ b/docs/shared/useBackgroundQuery-options.mdx @@ -89,6 +89,10 @@ If you're using [Apollo Link](/react/api/link/introduction/), this object is the +> **⚠️ Deprecated**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. The default value is `false`. diff --git a/docs/shared/useFragment-options.mdx b/docs/shared/useFragment-options.mdx index e3ca1c63e3b..75a9be37b48 100644 --- a/docs/shared/useFragment-options.mdx +++ b/docs/shared/useFragment-options.mdx @@ -108,6 +108,10 @@ Each key in the object corresponds to a variable name, and that key's value corr +> **⚠️ Deprecated**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. The default value is `false`. diff --git a/docs/shared/useSuspenseQuery-options.mdx b/docs/shared/useSuspenseQuery-options.mdx index c87dc25c544..38d6189d21f 100644 --- a/docs/shared/useSuspenseQuery-options.mdx +++ b/docs/shared/useSuspenseQuery-options.mdx @@ -84,6 +84,10 @@ If you're using [Apollo Link](/react/api/link/introduction/), this object is the +> **⚠️ Deprecated**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. The default value is `false`. diff --git a/docs/source/api/cache/InMemoryCache.mdx b/docs/source/api/cache/InMemoryCache.mdx index 0bd104013ee..0cae2424737 100644 --- a/docs/source/api/cache/InMemoryCache.mdx +++ b/docs/source/api/cache/InMemoryCache.mdx @@ -140,6 +140,10 @@ By specifying the ID of another cached object, you can query arbitrary cached da +> **⚠️ Deprecated**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. The default value is `false`. @@ -591,6 +595,10 @@ A map of any GraphQL variable names and values required by `fragment`. +> **⚠️ Deprecated**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. The default value is `false`. diff --git a/docs/source/api/react/hooks.mdx b/docs/source/api/react/hooks.mdx index 9466e50f24d..9a729b1c3a6 100644 --- a/docs/source/api/react/hooks.mdx +++ b/docs/source/api/react/hooks.mdx @@ -443,7 +443,6 @@ function useFragment< optimistic?: boolean; variables?: TVars; returnPartialData?: boolean; - canonizeResults?: boolean; }): UseFragmentResult {} ``` diff --git a/docs/source/caching/garbage-collection.mdx b/docs/source/caching/garbage-collection.mdx index b77643a255a..80fc7e768a8 100644 --- a/docs/source/caching/garbage-collection.mdx +++ b/docs/source/caching/garbage-collection.mdx @@ -33,6 +33,10 @@ cache.gc({ }) ``` +> **⚠️ Deprecation warning for `canonizeResults**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + These additional `cache.gc` options can be useful for investigating memory usage patterns or leaks. Before taking heap snapshots or recording allocation timelines, it's a good idea to force _JavaScript_ garbage collection using your browser's devtools, to ensure memory released by the cache has been fully collected and returned to the heap. ### Configuring garbage collection diff --git a/src/cache/core/types/Cache.ts b/src/cache/core/types/Cache.ts index 58835e6aca5..0fa70742e15 100644 --- a/src/cache/core/types/Cache.ts +++ b/src/cache/core/types/Cache.ts @@ -14,6 +14,13 @@ export namespace Cache { previousResult?: any; optimistic: boolean; returnPartialData?: boolean; + /** + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature without + * the risk of memory leaks. + */ canonizeResults?: boolean; } diff --git a/src/cache/core/types/DataProxy.ts b/src/cache/core/types/DataProxy.ts index 6dbdf47b75d..d340f187d4e 100644 --- a/src/cache/core/types/DataProxy.ts +++ b/src/cache/core/types/DataProxy.ts @@ -69,6 +69,13 @@ export namespace DataProxy { */ optimistic?: boolean; /** + /** + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature without + * the risk of memory leaks. + * * Whether to canonize cache results before returning them. Canonization * takes some extra time, but it speeds up future deep equality comparisons. * Defaults to false. @@ -90,6 +97,11 @@ export namespace DataProxy { */ optimistic?: boolean; /** + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature. + * * Whether to canonize cache results before returning them. Canonization * takes some extra time, but it speeds up future deep equality comparisons. * Defaults to false. diff --git a/src/cache/inmemory/types.ts b/src/cache/inmemory/types.ts index 207a802feb4..bd05ff7aacf 100644 --- a/src/cache/inmemory/types.ts +++ b/src/cache/inmemory/types.ts @@ -119,6 +119,13 @@ export type ReadQueryOptions = { query: DocumentNode; variables?: Object; previousResult?: any; + /** + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature without + * the risk of memory leaks. + */ canonizeResults?: boolean; rootId?: string; config?: ApolloReducerConfig; @@ -143,6 +150,12 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { * TODO: write docs page, add link here */ resultCacheMaxSize?: number; + /** + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature. + */ canonizeResults?: boolean; fragments?: FragmentRegistryAPI; } diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index fc722c5ed9c..c4844826c75 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -106,6 +106,12 @@ export interface QueryOptions { partialRefetch?: boolean; /** + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature without + * the risk of memory leaks. + * * Whether to canonize cache results before returning them. Canonization * takes some extra time, but it speeds up future deep equality comparisons. * Defaults to false. diff --git a/src/react/types/types.ts b/src/react/types/types.ts index f6f7af613aa..1d30b6f983b 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -197,6 +197,12 @@ export type LoadableQueryHookFetchPolicy = Extract< export interface LoadableQueryHookOptions { /** + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature without + * the risk of memory leaks. + * * Whether to canonize cache results before returning them. Canonization * takes some extra time, but it speeds up future deep equality comparisons. * Defaults to false. From b07d9d821ad484921dcce5e6914bade9276a25ee Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 18 Dec 2023 10:56:50 -0700 Subject: [PATCH 62/90] Fix doc warning in useQueryRefHandlers --- src/react/hooks/useQueryRefHandlers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts index b7f58da2f23..1e76e5cdce1 100644 --- a/src/react/hooks/useQueryRefHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -31,13 +31,13 @@ export interface UseQueryRefHandlersResult< * @example * ```tsx * const MyComponent({ queryRef }) { - * const { refetch, fetchMore } = useQueryRefHandlers(queryRef) + * const { refetch, fetchMore } = useQueryRefHandlers(queryRef); * * // ... * } * ``` * - * @param queryRef a `QueryReference` returned from `useBackgroundQuery` or `createQueryPreloader`. + * @param queryRef - A `QueryReference` returned from `useBackgroundQuery`, `useLoadableQuery`, or `createQueryPreloader`. */ export function useQueryRefHandlers< TData = unknown, From b05a5a9d6ebd0b6d7b5504d6079c3bad86ce138b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 18 Dec 2023 10:57:48 -0700 Subject: [PATCH 63/90] Regenerate api report --- .api-reports/api-report-react.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 5fbb07582ff..8d1369f0464 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -2420,9 +2420,9 @@ interface WatchQueryOptions Date: Mon, 18 Dec 2023 12:22:02 -0700 Subject: [PATCH 64/90] Switch to use the beta tag --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index f51bce71873..75b62d8bedf 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,6 +1,6 @@ { "mode": "pre", - "tag": "alpha", + "tag": "beta", "initialVersions": { "@apollo/client": "3.8.3" }, From 2ba07acba04da9838f24ee80c33672396df0e007 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 18 Dec 2023 12:22:28 -0700 Subject: [PATCH 65/90] Reset package.json version to ensure changesets records the right version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 33d4fe292ca..d1decc50bda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.5", + "version": "3.8.8", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 3a2240febeb6882bcb797a152b7e83dbcb57c232 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 18 Dec 2023 12:39:23 -0700 Subject: [PATCH 66/90] Version Packages (beta) (#11417) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 8 +++ CHANGELOG.md | 122 ++++++++++++++++++++++++++++++++++++++++++-- package-lock.json | 4 +- package.json | 2 +- 4 files changed, 129 insertions(+), 7 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 75b62d8bedf..4f54090a3c4 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -10,19 +10,27 @@ "clean-items-smash", "cold-llamas-turn", "dirty-kids-crash", + "dirty-tigers-matter", "forty-cups-shop", "friendly-clouds-laugh", "hot-ducks-burn", + "mighty-coats-check", "polite-avocados-warn", "quick-hats-marry", + "rare-snakes-melt", "shaggy-ears-scream", "shaggy-sheep-pull", "sixty-boxes-rest", "sour-sheep-walk", "strong-terms-perform", + "swift-zoos-collect", "thick-mice-collect", + "thick-tips-cry", "thirty-ties-arrive", + "tough-timers-begin", + "unlucky-rats-decide", "violet-lions-draw", + "wet-forks-rhyme", "wild-dolphins-jog", "yellow-flies-repeat" ] diff --git a/CHANGELOG.md b/CHANGELOG.md index c7326ece2c7..535667bf6e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,119 @@ # @apollo/client +## 3.9.0-beta.0 + +### Minor Changes + +- [#11412](https://github.com/apollographql/apollo-client/pull/11412) [`58db5c3`](https://github.com/apollographql/apollo-client/commit/58db5c3295b88162f91019f0898f6baa4b9cced6) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Create a new `useQueryRefHandlers` hook that returns `refetch` and `fetchMore` functions for a given `queryRef`. This is useful to get access to handlers for a `queryRef` that was created by `createQueryPreloader` or when the handlers for a `queryRef` produced by a different component are inaccessible. + + ```jsx + const MyComponent({ queryRef }) { + const { refetch, fetchMore } = useQueryRefHandlers(queryRef); + + // ... + } + ``` + +- [#11410](https://github.com/apollographql/apollo-client/pull/11410) [`07fcf6a`](https://github.com/apollographql/apollo-client/commit/07fcf6a3bf5bc78ffe6f3e598897246b4da02cbb) Thanks [@sf-twingate](https://github.com/sf-twingate)! - Allow returning `IGNORE` sentinel object from `optimisticResponse` functions to bail-out from the optimistic update. + + Consider this example: + + ```jsx + const UPDATE_COMMENT = gql` + mutation UpdateComment($commentId: ID!, $commentContent: String!) { + updateComment(commentId: $commentId, content: $commentContent) { + id + __typename + content + } + } + `; + + function CommentPageWithData() { + const [mutate] = useMutation(UPDATE_COMMENT); + return ( + + mutate({ + variables: { commentId, commentContent }, + optimisticResponse: (vars, { IGNORE }) => { + if (commentContent === "foo") { + // conditionally bail out of optimistic updates + return IGNORE; + } + return { + updateComment: { + id: commentId, + __typename: "Comment", + content: commentContent, + }, + }; + }, + }) + } + /> + ); + } + ``` + + The `IGNORE` sentinel can be destructured from the second parameter in the callback function signature passed to `optimisticResponse`. + +- [#11412](https://github.com/apollographql/apollo-client/pull/11412) [`58db5c3`](https://github.com/apollographql/apollo-client/commit/58db5c3295b88162f91019f0898f6baa4b9cced6) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add the ability to start preloading a query outside React to begin fetching as early as possible. Call `createQueryPreloader` to create a `preloadQuery` function which can be called to start fetching a query. This returns a `queryRef` which is passed to `useReadQuery` and suspended until the query is done fetching. + + ```tsx + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(QUERY, { variables, ...otherOptions }); + + function App() { + return { + Loading}> + + + } + } + + function MyQuery() { + const { data } = useReadQuery(queryRef); + + // do something with data + } + ``` + +- [#11397](https://github.com/apollographql/apollo-client/pull/11397) [`3f7eecb`](https://github.com/apollographql/apollo-client/commit/3f7eecbfbd4f4444cffcaac7dd9fd225c8c2a401) Thanks [@aditya-kumawat](https://github.com/aditya-kumawat)! - Adds a new `skipPollAttempt` callback function that's called whenever a refetch attempt occurs while polling. If the function returns `true`, the refetch is skipped and not reattempted until the next poll interval. This will solve the frequent use-case of disabling polling when the window is inactive. + + ```ts + useQuery(QUERY, { + pollInterval: 1000, + skipPollAttempt: () => document.hidden, // or !document.hasFocus() + }); + // or define it globally + new ApolloClient({ + defaultOptions: { + watchQuery: { + skipPollAttempt: () => document.hidden, // or !document.hasFocus() + }, + }, + }); + ``` + +- [#11435](https://github.com/apollographql/apollo-client/pull/11435) [`5cce53e`](https://github.com/apollographql/apollo-client/commit/5cce53e83b976f85d2d2b06e28cc38f01324fea1) Thanks [@phryneas](https://github.com/phryneas)! - Deprecates `canonizeResults`. + + Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. + A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + +### Patch Changes + +- [#11369](https://github.com/apollographql/apollo-client/pull/11369) [`2a47164`](https://github.com/apollographql/apollo-client/commit/2a471646616e3af1b5c039e961f8d5717fad8f32) Thanks [@phryneas](https://github.com/phryneas)! - Persisted Query Link: improve memory management + + - use LRU `WeakCache` instead of `WeakMap` to keep a limited number of hash results + - hash cache is initiated lazily, only when needed + - expose `persistedLink.resetHashCache()` method + - reset hash cache if the upstream server reports it doesn't accept persisted queries + +- [#10804](https://github.com/apollographql/apollo-client/pull/10804) [`221dd99`](https://github.com/apollographql/apollo-client/commit/221dd99ffd1990f8bd0392543af35e9b08d0fed8) Thanks [@phryneas](https://github.com/phryneas)! - use WeakMap in React Native with Hermes + +- [#11409](https://github.com/apollographql/apollo-client/pull/11409) [`2e7203b`](https://github.com/apollographql/apollo-client/commit/2e7203b3a9618952ddb522627ded7cceabd7f250) Thanks [@phryneas](https://github.com/phryneas)! - Adds an experimental `ApolloClient.getMemoryInternals` helper + ## 3.9.0-alpha.5 ### Minor Changes @@ -95,7 +209,7 @@ import { Environment, Network, RecordSource, Store } from "relay-runtime"; const fetchMultipartSubs = createFetchMultipartSubscription( - "http://localhost:4000" + "http://localhost:4000", ); const network = Network.create(fetchQuery, fetchMultipartSubs); @@ -377,7 +491,7 @@ return data.breeds.map(({ characteristics }) => characteristics.map((characteristic) => (
{characteristic}
- )) + )), ); } ``` @@ -428,7 +542,7 @@ const { data } = useSuspenseQuery( query, - id ? { variables: { id } } : skipToken + id ? { variables: { id } } : skipToken, ); ``` @@ -2383,7 +2497,7 @@ In upcoming v3.6.x and v3.7 (beta) releases, we will be completely overhauling o fields: { comments(comments: Reference[], { readField }) { return comments.filter( - (comment) => idToRemove !== readField("id", comment) + (comment) => idToRemove !== readField("id", comment), ); }, }, diff --git a/package-lock.json b/package-lock.json index b734c3c22f9..8c05475ae72 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.0-alpha.5", + "version": "3.9.0-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.0-alpha.5", + "version": "3.9.0-beta.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index d1decc50bda..2d43aee4708 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.8.8", + "version": "3.9.0-beta.0", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 33454f0a40a05ea2b00633bda20a84d0ec3a4f4d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 19 Dec 2023 07:48:34 -0700 Subject: [PATCH 67/90] Add `react/internal` entry point and update existing imports (#11439) --- .api-reports/api-report-react.md | 12 +- .api-reports/api-report-react_hooks.md | 6 +- .api-reports/api-report-react_internal.md | 1705 +++++++++++++++++ .api-reports/api-report.md | 6 +- .changeset/spicy-drinks-camp.md | 5 + .size-limits.json | 4 +- config/entryPoints.js | 1 + src/__tests__/__snapshots__/exports.ts.snap | 11 + src/__tests__/exports.ts | 2 + src/react/cache/index.ts | 2 - .../__tests__/useBackgroundQuery.test.tsx | 2 +- .../__tests__/useQueryRefHandlers.test.tsx | 2 +- src/react/hooks/useBackgroundQuery.ts | 7 +- src/react/hooks/useLoadableQuery.ts | 7 +- src/react/hooks/useQueryRefHandlers.ts | 4 +- src/react/hooks/useReadQuery.ts | 4 +- src/react/hooks/useSuspenseQuery.ts | 4 +- .../{ => internal}/cache/QueryReference.ts | 10 +- .../{ => internal}/cache/SuspenseCache.ts | 4 +- .../cache/__tests__/QueryReference.test.ts | 4 +- .../{ => internal}/cache/getSuspenseCache.ts | 6 +- src/react/{ => internal}/cache/types.ts | 0 src/react/internal/index.ts | 11 + .../__tests__/createQueryPreloader.test.tsx | 2 +- .../query-preloader/createQueryPreloader.ts | 7 +- src/react/types/types.ts | 2 +- src/testing/matchers/toBeDisposed.ts | 4 +- .../matchers/toHaveSuspenseCacheEntryUsing.ts | 4 +- 28 files changed, 1783 insertions(+), 55 deletions(-) create mode 100644 .api-reports/api-report-react_internal.md create mode 100644 .changeset/spicy-drinks-camp.md delete mode 100644 src/react/cache/index.ts rename src/react/{ => internal}/cache/QueryReference.ts (97%) rename src/react/{ => internal}/cache/SuspenseCache.ts (92%) rename src/react/{ => internal}/cache/__tests__/QueryReference.test.ts (87%) rename src/react/{ => internal}/cache/getSuspenseCache.ts (75%) rename src/react/{ => internal}/cache/types.ts (100%) create mode 100644 src/react/internal/index.ts diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 8d1369f0464..c6de1672ff0 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -2417,12 +2417,12 @@ interface WatchQueryOptions Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ASTNode } from 'graphql'; +import type { DocumentNode } from 'graphql'; +import type { ExecutionResult } from 'graphql'; +import type { FieldNode } from 'graphql'; +import type { FragmentDefinitionNode } from 'graphql'; +import type { GraphQLError } from 'graphql'; +import type { GraphQLErrorExtensions } from 'graphql'; +import { Observable } from 'zen-observable-ts'; +import type { Observer } from 'zen-observable-ts'; +import type { Subscriber } from 'zen-observable-ts'; +import type { Subscription } from 'zen-observable-ts'; +import { Trie } from '@wry/trie'; +import { TypedDocumentNode } from '@graphql-typed-document-node/core'; + +// Warning: (ae-forgotten-export) The symbol "Modifier" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "StoreObjectValueMaybeReference" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type AllFieldsModifier> = Modifier> : never>; + +// Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +abstract class ApolloCache implements DataProxy { + // (undocumented) + readonly assumeImmutableResults: boolean; + // (undocumented) + batch(options: Cache_2.BatchOptions): U; + // (undocumented) + abstract diff(query: Cache_2.DiffOptions): Cache_2.DiffResult; + // (undocumented) + abstract evict(options: Cache_2.EvictOptions): boolean; + abstract extract(optimistic?: boolean): TSerialized; + // (undocumented) + gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; + // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts + // + // (undocumented) + identify(object: StoreObject | Reference): string | undefined; + // (undocumented) + modify = Record>(options: Cache_2.ModifyOptions): boolean; + // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts + // + // (undocumented) + abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; + // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + abstract read(query: Cache_2.ReadOptions): TData | null; + // (undocumented) + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + // (undocumented) + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + // (undocumented) + recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; + // (undocumented) + abstract removeOptimistic(id: string): void; + // (undocumented) + abstract reset(options?: Cache_2.ResetOptions): Promise; + abstract restore(serializedState: TSerialized): ApolloCache; + // (undocumented) + transformDocument(document: DocumentNode): DocumentNode; + // (undocumented) + transformForLink(document: DocumentNode): DocumentNode; + // (undocumented) + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + // (undocumented) + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + // (undocumented) + abstract watch(watch: Cache_2.WatchOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts + // + // (undocumented) + abstract write(write: Cache_2.WriteOptions): Reference | undefined; + // (undocumented) + writeFragment({ id, data, fragment, fragmentName, ...options }: Cache_2.WriteFragmentOptions): Reference | undefined; + // (undocumented) + writeQuery({ id, data, ...options }: Cache_2.WriteQueryOptions): Reference | undefined; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "Observable" +// +// @public +class ApolloClient implements DataProxy { + // (undocumented) + __actionHookForDevTools(cb: () => any): void; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + constructor(options: ApolloClientOptions); + // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts + // + // (undocumented) + __requestRaw(payload: GraphQLRequest): Observable; + // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts + addResolvers(resolvers: Resolvers | Resolvers[]): void; + // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts + // + // (undocumented) + cache: ApolloCache; + clearStore(): Promise; + // (undocumented) + get defaultContext(): Partial; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + disableNetworkFetches: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts + get documentTransform(): DocumentTransform; + extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloClientMemoryInternals; + // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts + getObservableQueries(include?: RefetchQueriesInclude): Map>; + getResolvers(): Resolvers; + // Warning: (ae-forgotten-export) The symbol "ApolloLink" needs to be exported by the entry point index.d.ts + // + // (undocumented) + link: ApolloLink; + // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + onClearStore(cb: () => Promise): () => void; + onResetStore(cb: () => Promise): () => void; + // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "QueryOptions" + query(options: QueryOptions): Promise>; + // (undocumented) + queryDeduplication: boolean; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + reFetchObservableQueries(includeStandby?: boolean): Promise[]>; + // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts + refetchQueries = ApolloCache, TResult = Promise>>(options: RefetchQueriesOptions): RefetchQueriesResult; + resetStore(): Promise[] | null>; + restore(serializedState: TCacheShape): ApolloCache; + setLink(newLink: ApolloLink): void; + // Warning: (ae-forgotten-export) The symbol "FragmentMatcher" needs to be exported by the entry point index.d.ts + setLocalStateFragmentMatcher(fragmentMatcher: FragmentMatcher): void; + setResolvers(resolvers: Resolvers | Resolvers[]): void; + stop(): void; + // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "Observable" + subscribe(options: SubscriptionOptions): Observable>; + // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly typeDefs: ApolloClientOptions["typeDefs"]; + // (undocumented) + version: string; + // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchQueryOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ObservableQuery" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ObservableQuery" + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ObservableQuery" + watchQuery(options: WatchQueryOptions): ObservableQuery; + writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; + writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; +} + +// @public (undocumented) +interface ApolloClientOptions { + assumeImmutableResults?: boolean; + cache: ApolloCache; + connectToDevTools?: boolean; + // (undocumented) + credentials?: string; + // (undocumented) + defaultContext?: Partial; + defaultOptions?: DefaultOptions; + // (undocumented) + documentTransform?: DocumentTransform; + // (undocumented) + fragmentMatcher?: FragmentMatcher; + // (undocumented) + headers?: Record; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" + link?: ApolloLink; + name?: string; + queryDeduplication?: boolean; + // (undocumented) + resolvers?: Resolvers | Resolvers[]; + ssrForceFetchDelay?: number; + ssrMode?: boolean; + // (undocumented) + typeDefs?: string | string[] | DocumentNode | DocumentNode[]; + // Warning: (ae-forgotten-export) The symbol "UriFunction" needs to be exported by the entry point index.d.ts + uri?: string | UriFunction; + version?: string; +} + +// @public (undocumented) +class ApolloError extends Error { + // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts + constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + // (undocumented) + clientErrors: ReadonlyArray; + // (undocumented) + extraInfo: any; + // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts + // + // (undocumented) + graphQLErrors: GraphQLErrors; + // (undocumented) + message: string; + // (undocumented) + name: string; + // Warning: (ae-forgotten-export) The symbol "ServerParseError" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ServerError" needs to be exported by the entry point index.d.ts + // + // (undocumented) + networkError: Error | ServerParseError | ServerError | null; + // (undocumented) + protocolErrors: ReadonlyArray<{ + message: string; + extensions?: GraphQLErrorExtensions[]; + }>; +} + +// @public (undocumented) +interface ApolloErrorOptions { + // (undocumented) + clientErrors?: ReadonlyArray; + // (undocumented) + errorMessage?: string; + // (undocumented) + extraInfo?: any; + // (undocumented) + graphQLErrors?: ReadonlyArray; + // (undocumented) + networkError?: Error | ServerParseError | ServerError | null; + // (undocumented) + protocolErrors?: ReadonlyArray<{ + message: string; + extensions?: GraphQLErrorExtensions[]; + }>; +} + +// @public (undocumented) +class ApolloLink { + constructor(request?: RequestHandler); + // (undocumented) + static concat(first: ApolloLink | RequestHandler, second: ApolloLink | RequestHandler): ApolloLink; + // (undocumented) + concat(next: ApolloLink | RequestHandler): ApolloLink; + // (undocumented) + static empty(): ApolloLink; + // (undocumented) + static execute(link: ApolloLink, operation: GraphQLRequest): Observable; + // Warning: (ae-forgotten-export) The symbol "RequestHandler" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; + // (undocumented) + protected onError(error: any, observer?: Observer): false | void; + // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts + // + // (undocumented) + request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; + // (undocumented) + setOnError(fn: ApolloLink["onError"]): this; + // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static split(test: (op: Operation) => boolean, left: ApolloLink | RequestHandler, right?: ApolloLink | RequestHandler): ApolloLink; + // (undocumented) + split(test: (op: Operation) => boolean, left: ApolloLink | RequestHandler, right?: ApolloLink | RequestHandler): ApolloLink; +} + +// @public (undocumented) +type ApolloQueryResult = { + data: T; + errors?: ReadonlyArray; + error?: ApolloError; + loading: boolean; + networkStatus: NetworkStatus; + partial?: boolean; +}; + +// @public +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + +// @public (undocumented) +namespace Cache_2 { + // (undocumented) + interface BatchOptions, TUpdateResult = void> { + // (undocumented) + onWatchUpdated?: (this: TCache, watch: Cache_2.WatchOptions, diff: Cache_2.DiffResult, lastDiff?: Cache_2.DiffResult | undefined) => any; + // (undocumented) + optimistic?: string | boolean; + // (undocumented) + removeOptimistic?: string; + // (undocumented) + update(cache: TCache): TUpdateResult; + } + // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface DiffOptions extends Omit, "rootId"> { + } + // (undocumented) + interface EvictOptions { + // (undocumented) + args?: Record; + // (undocumented) + broadcast?: boolean; + // (undocumented) + fieldName?: string; + // (undocumented) + id?: string; + } + // (undocumented) + interface ModifyOptions = Record> { + // (undocumented) + broadcast?: boolean; + // Warning: (ae-forgotten-export) The symbol "Modifiers" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "AllFieldsModifier" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fields: Modifiers | AllFieldsModifier; + // (undocumented) + id?: string; + // (undocumented) + optimistic?: boolean; + } + // (undocumented) + interface ReadOptions extends DataProxy.Query { + // @deprecated (undocumented) + canonizeResults?: boolean; + // (undocumented) + optimistic: boolean; + // (undocumented) + previousResult?: any; + // (undocumented) + returnPartialData?: boolean; + // (undocumented) + rootId?: string; + } + // (undocumented) + interface ResetOptions { + // (undocumented) + discardWatches?: boolean; + } + // (undocumented) + type WatchCallback = (diff: Cache_2.DiffResult, lastDiff?: Cache_2.DiffResult) => void; + // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface WatchOptions extends DiffOptions { + // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + callback: WatchCallback; + // (undocumented) + immediate?: boolean; + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + lastDiff?: DiffResult; + // (undocumented) + watcher?: object; + } + // (undocumented) + interface WriteOptions extends Omit, "id">, Omit, "data"> { + // (undocumented) + dataId?: string; + // (undocumented) + result: TResult; + } + import DiffResult = DataProxy.DiffResult; + import ReadQueryOptions = DataProxy.ReadQueryOptions; + import ReadFragmentOptions = DataProxy.ReadFragmentOptions; + import WriteQueryOptions = DataProxy.WriteQueryOptions; + import WriteFragmentOptions = DataProxy.WriteFragmentOptions; + import UpdateQueryOptions = DataProxy.UpdateQueryOptions; + import UpdateFragmentOptions = DataProxy.UpdateFragmentOptions; + import Fragment = DataProxy.Fragment; +} + +// @public (undocumented) +export type CacheKey = [ +query: DocumentNode, +stringifiedVariables: string, +...queryKey: any[] +]; + +// @public (undocumented) +const enum CacheWriteBehavior { + // (undocumented) + FORBID = 0, + // (undocumented) + MERGE = 2, + // (undocumented) + OVERWRITE = 1 +} + +// Warning: (ae-forgotten-export) The symbol "StoreValue" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CanReadFunction = (value: StoreValue) => boolean; + +// @public (undocumented) +class Concast extends Observable { + // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ConcastSourcesIterable" needs to be exported by the entry point index.d.ts + constructor(sources: MaybeAsync> | Subscriber); + // (undocumented) + addObserver(observer: Observer): void; + // Warning: (ae-forgotten-export) The symbol "NextResultListener" needs to be exported by the entry point index.d.ts + // + // (undocumented) + beforeNext(callback: NextResultListener): void; + // (undocumented) + cancel: (reason: any) => void; + // (undocumented) + readonly promise: Promise; + // (undocumented) + removeObserver(observer: Observer): void; +} + +// Warning: (ae-forgotten-export) The symbol "Source" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type ConcastSourcesIterable = Iterable>; + +// @public (undocumented) +namespace DataProxy { + // (undocumented) + type DiffResult = { + result?: T; + complete?: boolean; + missing?: MissingFieldError[]; + fromOptimisticTransaction?: boolean; + }; + // (undocumented) + interface Fragment { + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + id?: string; + variables?: TVariables; + } + // (undocumented) + interface Query { + id?: string; + query: DocumentNode | TypedDocumentNode; + variables?: TVariables; + } + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface ReadFragmentOptions extends Fragment { + // @deprecated (undocumented) + canonizeResults?: boolean; + optimistic?: boolean; + returnPartialData?: boolean; + } + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface ReadQueryOptions extends Query { + // @deprecated + canonizeResults?: boolean; + optimistic?: boolean; + returnPartialData?: boolean; + } + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface UpdateFragmentOptions extends Omit & WriteFragmentOptions, "data"> { + } + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface UpdateQueryOptions extends Omit & WriteQueryOptions, "data"> { + } + // (undocumented) + interface WriteFragmentOptions extends Fragment, WriteOptions { + } + // (undocumented) + interface WriteOptions { + broadcast?: boolean; + data: TData; + overwrite?: boolean; + } + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface WriteQueryOptions extends Query, WriteOptions { + } +} + +// @public +interface DataProxy { + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; + writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; +} + +// @public (undocumented) +interface DefaultContext extends Record { +} + +// @public (undocumented) +interface DefaultOptions { + // (undocumented) + mutate?: Partial>; + // (undocumented) + query?: Partial>; + // (undocumented) + watchQuery?: Partial>; +} + +// @public (undocumented) +interface DeleteModifier { + // (undocumented) + [_deleteModifier]: true; +} + +// @public (undocumented) +const _deleteModifier: unique symbol; + +// @public (undocumented) +class DocumentTransform { + // Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "DocumentTransformOptions" needs to be exported by the entry point index.d.ts + constructor(transform: TransformFn, options?: DocumentTransformOptions); + // (undocumented) + concat(otherTransform: DocumentTransform): DocumentTransform; + // (undocumented) + static identity(): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; + // (undocumented) + transformDocument(document: DocumentNode): DocumentNode; +} + +// @public (undocumented) +type DocumentTransformCacheKey = ReadonlyArray; + +// @public (undocumented) +interface DocumentTransformOptions { + // (undocumented) + cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; +} + +// @public +type ErrorPolicy = "none" | "ignore" | "all"; + +// Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface ExecutionPatchIncrementalResult, TExtensions = Record> extends ExecutionPatchResultBase { + // (undocumented) + data?: never; + // (undocumented) + errors?: never; + // (undocumented) + extensions?: never; + // Warning: (ae-forgotten-export) The symbol "IncrementalPayload" needs to be exported by the entry point index.d.ts + // + // (undocumented) + incremental?: IncrementalPayload[]; +} + +// @public (undocumented) +interface ExecutionPatchInitialResult, TExtensions = Record> extends ExecutionPatchResultBase { + // (undocumented) + data: TData | null | undefined; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; + // (undocumented) + incremental?: never; +} + +// Warning: (ae-forgotten-export) The symbol "ExecutionPatchInitialResult" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionPatchIncrementalResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type ExecutionPatchResult, TExtensions = Record> = ExecutionPatchInitialResult | ExecutionPatchIncrementalResult; + +// @public (undocumented) +interface ExecutionPatchResultBase { + // (undocumented) + hasNext?: boolean; +} + +// @public (undocumented) +type FetchMoreOptions = Parameters["fetchMore"]>[0]; + +// @public (undocumented) +interface FetchMoreQueryOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) + query?: DocumentNode | TypedDocumentNode; + // (undocumented) + variables?: Partial; +} + +// @public +type FetchPolicy = "cache-first" | "network-only" | "cache-only" | "no-cache" | "standby"; + +// Warning: (ae-forgotten-export) The symbol "SingleExecutionResult" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionPatchResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type FetchResult, TContext = Record, TExtensions = Record> = SingleExecutionResult | ExecutionPatchResult; + +// @public (undocumented) +interface FieldSpecifier { + // (undocumented) + args?: Record; + // (undocumented) + field?: FieldNode; + // (undocumented) + fieldName: string; + // (undocumented) + typename?: string; + // (undocumented) + variables?: Record; +} + +// @public +interface FragmentMap { + // (undocumented) + [fragmentName: string]: FragmentDefinitionNode; +} + +// @public (undocumented) +type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; + +// @public (undocumented) +interface FulfilledPromise extends Promise { + // (undocumented) + status: "fulfilled"; + // (undocumented) + value: TValue; +} + +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + +// Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SuspenseCache" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function getSuspenseCache(client: ApolloClient & { + [suspenseCacheSymbol]?: SuspenseCache; +}): SuspenseCache; + +// Warning: (ae-forgotten-export) The symbol "QueryRefPromise" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function getWrappedPromise(queryRef: QueryReference): QueryRefPromise; + +// @public (undocumented) +type GraphQLErrors = ReadonlyArray; + +// @public (undocumented) +interface GraphQLRequest> { + // (undocumented) + context?: DefaultContext; + // (undocumented) + extensions?: Record; + // (undocumented) + operationName?: string; + // (undocumented) + query: DocumentNode; + // (undocumented) + variables?: TVariables; +} + +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + +// @public (undocumented) +interface IncrementalPayload { + // (undocumented) + data: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; + // (undocumented) + label?: string; + // Warning: (ae-forgotten-export) The symbol "Path" needs to be exported by the entry point index.d.ts + // + // (undocumented) + path: Path; +} + +// @public (undocumented) +export class InternalQueryReference { + // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts + constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); + // (undocumented) + applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; + // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + didChangeOptions(watchQueryOptions: ObservedOptions): boolean; + // (undocumented) + get disposed(): boolean; + // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fetchMore(options: FetchMoreOptions): Promise>; + // (undocumented) + readonly key: QueryKey; + // Warning: (ae-forgotten-export) The symbol "Listener" needs to be exported by the entry point index.d.ts + // + // (undocumented) + listen(listener: Listener): () => void; + // (undocumented) + readonly observable: ObservableQuery; + // (undocumented) + promise: QueryRefPromise; + // (undocumented) + refetch(variables: OperationVariables | undefined): Promise>; + // (undocumented) + reinitialize(): void; + // (undocumented) + result: ApolloQueryResult; + // (undocumented) + retain(): () => void; + // (undocumented) + get watchQueryOptions(): WatchQueryOptions; +} + +// @public (undocumented) +interface InternalQueryReferenceOptions { + // (undocumented) + autoDisposeTimeoutMs?: number; + // (undocumented) + onDispose?: () => void; +} + +// Warning: (ae-forgotten-export) The symbol "InternalRefetchQueryDescriptor" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RefetchQueriesIncludeShorthand" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type InternalRefetchQueriesInclude = InternalRefetchQueryDescriptor[] | RefetchQueriesIncludeShorthand; + +// Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type InternalRefetchQueriesMap = Map, InternalRefetchQueriesResult>; + +// @public (undocumented) +interface InternalRefetchQueriesOptions, TResult> extends Omit, "include"> { + // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesInclude" needs to be exported by the entry point index.d.ts + // + // (undocumented) + include?: InternalRefetchQueriesInclude; + // (undocumented) + removeOptimistic?: string; +} + +// @public (undocumented) +type InternalRefetchQueriesResult = TResult extends boolean ? Promise> : TResult; + +// Warning: (ae-forgotten-export) The symbol "RefetchQueryDescriptor" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type InternalRefetchQueryDescriptor = RefetchQueryDescriptor | QueryOptions; + +// @public (undocumented) +interface InvalidateModifier { + // (undocumented) + [_invalidateModifier]: true; +} + +// @public (undocumented) +const _invalidateModifier: unique symbol; + +// @public (undocumented) +function isReference(obj: any): obj is Reference; + +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type IsStrictlyAny = UnionToIntersection> extends never ? true : false; + +// @public (undocumented) +type Listener = (promise: QueryRefPromise) => void; + +// @public (undocumented) +class LocalState { + // Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts + constructor({ cache, client, resolvers, fragmentMatcher, }: LocalStateOptions); + // (undocumented) + addExportedVariables(document: DocumentNode, variables?: TVars, context?: {}): Promise; + // (undocumented) + addResolvers(resolvers: Resolvers | Resolvers[]): void; + // (undocumented) + clientQuery(document: DocumentNode): DocumentNode | null; + // (undocumented) + getFragmentMatcher(): FragmentMatcher | undefined; + // (undocumented) + getResolvers(): Resolvers; + // (undocumented) + prepareContext(context?: Record): { + cache: ApolloCache; + getCacheKey(obj: StoreObject): string | undefined; + }; + // (undocumented) + runResolvers({ document, remoteResult, context, variables, onlyRunForcedResolvers, }: { + document: DocumentNode | null; + remoteResult: FetchResult; + context?: Record; + variables?: Record; + onlyRunForcedResolvers?: boolean; + }): Promise>; + // (undocumented) + serverQuery(document: DocumentNode): DocumentNode | null; + // (undocumented) + setFragmentMatcher(fragmentMatcher: FragmentMatcher): void; + // (undocumented) + setResolvers(resolvers: Resolvers | Resolvers[]): void; + // (undocumented) + shouldForceResolvers(document: ASTNode): boolean; +} + +// @public (undocumented) +type LocalStateOptions = { + cache: ApolloCache; + client?: ApolloClient; + resolvers?: Resolvers | Resolvers[]; + fragmentMatcher?: FragmentMatcher; +}; + +// @public (undocumented) +type MaybeAsync = T | PromiseLike; + +// @public (undocumented) +class MissingFieldError extends Error { + constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); + // (undocumented) + readonly message: string; + // (undocumented) + readonly missing: MissingTree; + // Warning: (ae-forgotten-export) The symbol "MissingTree" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly path: MissingTree | Array; + // (undocumented) + readonly query: DocumentNode; + // (undocumented) + readonly variables?: Record | undefined; +} + +// @public (undocumented) +type MissingTree = string | { + readonly [key: string]: MissingTree; +}; + +// Warning: (ae-forgotten-export) The symbol "ModifierDetails" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeleteModifier" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "InvalidateModifier" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type Modifier = (value: T, details: ModifierDetails) => T | DeleteModifier | InvalidateModifier; + +// @public (undocumented) +type ModifierDetails = { + DELETE: DeleteModifier; + INVALIDATE: InvalidateModifier; + fieldName: string; + storeFieldName: string; + readField: ReadFieldFunction; + canRead: CanReadFunction; + isReference: typeof isReference; + toReference: ToReferenceFunction; + storage: StorageType; +}; + +// @public (undocumented) +type Modifiers = Record> = Partial<{ + [FieldName in keyof T]: Modifier>>; +}>; + +// @public (undocumented) +interface MutationBaseOptions = ApolloCache> { + awaitRefetchQueries?: boolean; + context?: TContext; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" + errorPolicy?: ErrorPolicy; + // Warning: (ae-forgotten-export) The symbol "OnQueryUpdated" needs to be exported by the entry point index.d.ts + onQueryUpdated?: OnQueryUpdated; + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); + refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" + update?: MutationUpdaterFunction; + // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" + updateQueries?: MutationQueryReducersMap; + variables?: TVariables; +} + +// Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type MutationFetchPolicy = Extract; + +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; + mutation: DocumentNode | TypedDocumentNode; +} + +// @public (undocumented) +type MutationQueryReducer = (previousResult: Record, options: { + mutationResult: FetchResult; + queryName: string | undefined; + queryVariables: Record; +}) => Record; + +// @public (undocumented) +type MutationQueryReducersMap = { + [queryName: string]: MutationQueryReducer; +}; + +// @public (undocumented) +interface MutationStoreValue { + // (undocumented) + error: Error | null; + // (undocumented) + loading: boolean; + // (undocumented) + mutation: DocumentNode; + // (undocumented) + variables: Record; +} + +// @public (undocumented) +type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { + context?: TContext; + variables?: TVariables; +}) => void; + +// @public +enum NetworkStatus { + error = 8, + fetchMore = 3, + loading = 1, + poll = 6, + ready = 7, + refetch = 4, + setVariables = 2 +} + +// @public (undocumented) +interface NextFetchPolicyContext { + // Warning: (ae-forgotten-export) The symbol "WatchQueryFetchPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + initialFetchPolicy: WatchQueryFetchPolicy; + // (undocumented) + observable: ObservableQuery; + // (undocumented) + options: WatchQueryOptions; + // (undocumented) + reason: "after-fetch" | "variables-changed"; +} + +// @public (undocumented) +type NextLink = (operation: Operation) => Observable; + +// @public (undocumented) +type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; + +// @public (undocumented) +class ObservableQuery extends Observable> { + constructor({ queryManager, queryInfo, options, }: { + queryManager: QueryManager; + queryInfo: QueryInfo; + options: WatchQueryOptions; + }); + // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + // (undocumented) + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + // (undocumented) + getLastError(variablesMustMatch?: boolean): ApolloError | undefined; + // (undocumented) + getLastResult(variablesMustMatch?: boolean): ApolloQueryResult | undefined; + // (undocumented) + hasObservers(): boolean; + // (undocumented) + isDifferentFromLastResult(newResult: ApolloQueryResult, variables?: TVariables): boolean | undefined; + // (undocumented) + readonly options: WatchQueryOptions; + // (undocumented) + get query(): TypedDocumentNode; + // (undocumented) + readonly queryId: string; + // (undocumented) + readonly queryName?: string; + refetch(variables?: Partial): Promise>; + // (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts + // + // (undocumented) + reobserveAsConcast(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; + // (undocumented) + resetLastResults(): void; + // (undocumented) + resetQueryStoreErrors(): void; + // (undocumented) + resubscribeAfterError(onNext: (value: ApolloQueryResult) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; + // (undocumented) + resubscribeAfterError(observer: Observer>): Subscription; + // (undocumented) + result(): Promise>; + // (undocumented) + setOptions(newOptions: Partial>): Promise>; + setVariables(variables: TVariables): Promise | void>; + // (undocumented) + silentSetOptions(newOptions: Partial>): void; + // (undocumented) + startPolling(pollInterval: number): void; + // (undocumented) + stopPolling(): void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // (undocumented) + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + // (undocumented) + get variables(): TVariables | undefined; +} + +// @public (undocumented) +const OBSERVED_CHANGED_OPTIONS: readonly ["canonizeResults", "context", "errorPolicy", "fetchPolicy", "refetchWritePolicy", "returnPartialData"]; + +// Warning: (ae-forgotten-export) The symbol "OBSERVED_CHANGED_OPTIONS" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type ObservedOptions = Pick; + +// @public (undocumented) +type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; + +// @public (undocumented) +interface Operation { + // (undocumented) + extensions: Record; + // (undocumented) + getContext: () => DefaultContext; + // (undocumented) + operationName: string; + // (undocumented) + query: DocumentNode; + // (undocumented) + setContext: (context: DefaultContext) => DefaultContext; + // (undocumented) + variables: Record; +} + +// @public (undocumented) +type OperationVariables = Record; + +// @public (undocumented) +type Path = ReadonlyArray; + +// @public (undocumented) +interface PendingPromise extends Promise { + // (undocumented) + status: "pending"; +} + +// @public (undocumented) +const PROMISE_SYMBOL: unique symbol; + +// Warning: (ae-forgotten-export) The symbol "PendingPromise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "FulfilledPromise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RejectedPromise" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type PromiseWithState = PendingPromise | FulfilledPromise | RejectedPromise; + +// @public (undocumented) +const QUERY_REFERENCE_SYMBOL: unique symbol; + +// @public (undocumented) +class QueryInfo { + constructor(queryManager: QueryManager, queryId?: string); + // (undocumented) + document: DocumentNode | null; + // (undocumented) + getDiff(): Cache_2.DiffResult; + // (undocumented) + graphQLErrors?: ReadonlyArray; + // (undocumented) + init(query: { + document: DocumentNode; + variables: Record | undefined; + networkStatus?: NetworkStatus; + observableQuery?: ObservableQuery; + lastRequestId?: number; + }): this; + // (undocumented) + lastRequestId: number; + // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts + // + // (undocumented) + listeners: Set; + // (undocumented) + markError(error: ApolloError): ApolloError; + // (undocumented) + markReady(): NetworkStatus; + // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts + // + // (undocumented) + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; + // (undocumented) + networkError?: Error | null; + // (undocumented) + networkStatus?: NetworkStatus; + // (undocumented) + notify(): void; + // (undocumented) + readonly observableQuery: ObservableQuery | null; + // (undocumented) + readonly queryId: string; + // (undocumented) + reset(): void; + // (undocumented) + resetDiff(): void; + // (undocumented) + resetLastWrite(): void; + // (undocumented) + setDiff(diff: Cache_2.DiffResult | null): void; + // (undocumented) + setObservableQuery(oq: ObservableQuery | null): void; + // (undocumented) + stop(): void; + // (undocumented) + stopped: boolean; + // (undocumented) + variables?: Record; +} + +// @public (undocumented) +export interface QueryKey { + // (undocumented) + __queryKey?: string; +} + +// @public (undocumented) +type QueryListener = (queryInfo: QueryInfo) => void; + +// @public (undocumented) +class QueryManager { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { + cache: ApolloCache; + link: ApolloLink; + defaultOptions?: DefaultOptions; + documentTransform?: DocumentTransform; + queryDeduplication?: boolean; + onBroadcast?: () => void; + ssrMode?: boolean; + clientAwareness?: Record; + localState?: LocalState; + assumeImmutableResults?: boolean; + defaultContext?: Partial; + }); + // (undocumented) + readonly assumeImmutableResults: boolean; + // (undocumented) + broadcastQueries(): void; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clearStore(options?: Cache_2.ResetOptions): Promise; + // (undocumented) + readonly defaultContext: Partial; + // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + readonly documentTransform: DocumentTransform; + // (undocumented) + protected fetchCancelFns: Map any>; + // (undocumented) + fetchQuery(queryId: string, options: WatchQueryOptions, networkStatus?: NetworkStatus): Promise>; + // (undocumented) + generateMutationId(): string; + // (undocumented) + generateQueryId(): string; + // (undocumented) + generateRequestId(): number; + // Warning: (ae-forgotten-export) The symbol "TransformCacheEntry" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // (undocumented) + getLocalState(): LocalState; + // (undocumented) + getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getQueryStore(): Record; + // (undocumented) + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; + // (undocumented) + link: ApolloLink; + // (undocumented) + markMutationOptimistic>(optimisticResponse: any, mutation: { + mutationId: string; + document: DocumentNode; + variables?: TVariables; + fetchPolicy?: MutationFetchPolicy; + errorPolicy: ErrorPolicy; + context?: TContext; + updateQueries: UpdateQueries; + update?: MutationUpdaterFunction; + keepRootFields?: boolean; + }): boolean; + // (undocumented) + markMutationResult>(mutation: { + mutationId: string; + result: FetchResult; + document: DocumentNode; + variables?: TVariables; + fetchPolicy?: MutationFetchPolicy; + errorPolicy: ErrorPolicy; + context?: TContext; + updateQueries: UpdateQueries; + update?: MutationUpdaterFunction; + awaitRefetchQueries?: boolean; + refetchQueries?: InternalRefetchQueriesInclude; + removeOptimistic?: string; + onQueryUpdated?: OnQueryUpdated; + keepRootFields?: boolean; + }, cache?: ApolloCache): Promise>; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + // (undocumented) + mutationStore?: { + [mutationId: string]: MutationStoreValue; + }; + // (undocumented) + query(options: QueryOptions, queryId?: string): Promise>; + // (undocumented) + reFetchObservableQueries(includeStandby?: boolean): Promise[]>; + // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesMap" needs to be exported by the entry point index.d.ts + // + // (undocumented) + refetchQueries({ updateCache, include, optimistic, removeOptimistic, onQueryUpdated, }: InternalRefetchQueriesOptions, TResult>): InternalRefetchQueriesMap; + // (undocumented) + removeQuery(queryId: string): void; + // (undocumented) + resetErrors(queryId: string): void; + // (undocumented) + setObservableQuery(observableQuery: ObservableQuery): void; + // (undocumented) + readonly ssrMode: boolean; + // (undocumented) + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + stop(): void; + // (undocumented) + stopQuery(queryId: string): void; + // (undocumented) + stopQueryInStore(queryId: string): void; + // (undocumented) + transform(document: DocumentNode): DocumentNode; + // (undocumented) + watchQuery(options: WatchQueryOptions): ObservableQuery; +} + +// @public +interface QueryOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + context?: DefaultContext; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" + errorPolicy?: ErrorPolicy; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + fetchPolicy?: FetchPolicy; + notifyOnNetworkStatusChange?: boolean; + partialRefetch?: boolean; + pollInterval?: number; + query: DocumentNode | TypedDocumentNode; + returnPartialData?: boolean; + variables?: TVariables; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "useBackgroundQuery" +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "useReadQuery" +// +// @public +export interface QueryReference { + // (undocumented) + [PROMISE_SYMBOL]: QueryRefPromise; + // (undocumented) + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // (undocumented) + toPromise(): Promise>; +} + +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type QueryRefPromise = PromiseWithState>; + +// @public (undocumented) +type QueryStoreValue = Pick; + +// @public (undocumented) +interface ReadFieldFunction { + // Warning: (ae-forgotten-export) The symbol "ReadFieldOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "SafeReadonly" needs to be exported by the entry point index.d.ts + // + // (undocumented) + (options: ReadFieldOptions): SafeReadonly | undefined; + // (undocumented) + (fieldName: string, from?: StoreObject | Reference): SafeReadonly | undefined; +} + +// Warning: (ae-forgotten-export) The symbol "FieldSpecifier" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface ReadFieldOptions extends FieldSpecifier { + // (undocumented) + from?: StoreObject | Reference; +} + +// @public (undocumented) +interface Reference { + // (undocumented) + readonly __ref: string; +} + +// @public (undocumented) +type RefetchQueriesInclude = RefetchQueryDescriptor[] | RefetchQueriesIncludeShorthand; + +// @public (undocumented) +type RefetchQueriesIncludeShorthand = "all" | "active"; + +// @public (undocumented) +interface RefetchQueriesOptions, TResult> { + // (undocumented) + include?: RefetchQueriesInclude; + // (undocumented) + onQueryUpdated?: OnQueryUpdated | null; + // (undocumented) + optimistic?: boolean; + // (undocumented) + updateCache?: (cache: TCache) => void; +} + +// Warning: (ae-forgotten-export) The symbol "IsStrictlyAny" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type RefetchQueriesPromiseResults = IsStrictlyAny extends true ? any[] : TResult extends boolean ? ApolloQueryResult[] : TResult extends PromiseLike ? U[] : TResult[]; + +// Warning: (ae-forgotten-export) The symbol "RefetchQueriesPromiseResults" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface RefetchQueriesResult extends Promise> { + // (undocumented) + queries: ObservableQuery[]; + // (undocumented) + results: InternalRefetchQueriesResult[]; +} + +// @public (undocumented) +type RefetchQueryDescriptor = string | DocumentNode; + +// @public (undocumented) +type RefetchWritePolicy = "merge" | "overwrite"; + +// @public (undocumented) +interface RejectedPromise extends Promise { + // (undocumented) + reason: unknown; + // (undocumented) + status: "rejected"; +} + +// @public (undocumented) +type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; + +// @public (undocumented) +type Resolver = (rootValue?: any, args?: any, context?: any, info?: { + field: FieldNode; + fragmentMap: FragmentMap; +}) => any; + +// @public (undocumented) +interface Resolvers { + // (undocumented) + [key: string]: { + [field: string]: Resolver; + }; +} + +// @public (undocumented) +type SafeReadonly = T extends object ? Readonly : T; + +// @public (undocumented) +type ServerError = Error & { + response: Response; + result: Record | string; + statusCode: number; +}; + +// @public (undocumented) +type ServerParseError = Error & { + response: Response; + statusCode: number; + bodyText: string; +}; + +// @public (undocumented) +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { + // (undocumented) + context?: TContext; + // (undocumented) + data?: TData | null; +} + +// @public (undocumented) +type Source = MaybeAsync>; + +// @public (undocumented) +type StorageType = Record; + +// @public (undocumented) +interface StoreObject { + // (undocumented) + [storeFieldName: string]: StoreValue; + // (undocumented) + __typename?: string; +} + +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type StoreObjectValueMaybeReference = StoreVal extends Array> ? StoreVal extends Array ? Item extends Record ? ReadonlyArray | Reference> : never : never : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; + +// @public (undocumented) +type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; + +// @public (undocumented) +type SubscribeToMoreOptions = { + document: DocumentNode | TypedDocumentNode; + variables?: TSubscriptionVariables; + updateQuery?: UpdateQueryFn; + onError?: (error: Error) => void; + context?: DefaultContext; +}; + +// @public (undocumented) +interface SubscriptionOptions { + context?: DefaultContext; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" + errorPolicy?: ErrorPolicy; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + fetchPolicy?: FetchPolicy; + query: DocumentNode | TypedDocumentNode; + variables?: TVariables; +} + +// @public (undocumented) +class SuspenseCache { + constructor(options?: SuspenseCacheOptions); + // (undocumented) + getQueryRef(cacheKey: CacheKey, createObservable: () => ObservableQuery): InternalQueryReference; +} + +// @public (undocumented) +export interface SuspenseCacheOptions { + autoDisposeTimeoutMs?: number; +} + +// @public (undocumented) +const suspenseCacheSymbol: unique symbol; + +// @public (undocumented) +type ToReferenceFunction = (objOrIdOrRef: StoreObject | string | Reference, mergeIntoStore?: boolean) => Reference | undefined; + +// @public (undocumented) +type Transaction = (c: ApolloCache) => void; + +// @public (undocumented) +interface TransformCacheEntry { + // (undocumented) + asQuery: DocumentNode; + // (undocumented) + clientQuery: DocumentNode | null; + // (undocumented) + defaultVars: OperationVariables; + // (undocumented) + hasClientExports: boolean; + // (undocumented) + hasForcedResolvers: boolean; + // (undocumented) + hasNonreactiveDirective: boolean; + // (undocumented) + serverQuery: DocumentNode | null; +} + +// @public (undocumented) +type TransformFn = (document: DocumentNode) => DocumentNode; + +// @public (undocumented) +type UnionForAny = T extends never ? "a" : 1; + +// @public (undocumented) +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public (undocumented) +export function unwrapQueryRef(queryRef: QueryReference): InternalQueryReference; + +// @public (undocumented) +type UpdateQueries = MutationOptions["updateQueries"]; + +// @public (undocumented) +type UpdateQueryFn = (previousQueryResult: TData, options: { + subscriptionData: { + data: TSubscriptionData; + }; + variables?: TSubscriptionVariables; +}) => TData; + +// @public (undocumented) +export function updateWrappedQueryRef(queryRef: QueryReference, promise: QueryRefPromise): void; + +// @public (undocumented) +interface UriFunction { + // (undocumented) + (operation: Operation): string; +} + +// @public (undocumented) +type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; + +// @public +interface WatchQueryOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + context?: DefaultContext; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" + errorPolicy?: ErrorPolicy; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + partialRefetch?: boolean; + pollInterval?: number; + query: DocumentNode | TypedDocumentNode; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + +// @public (undocumented) +export function wrapQueryRef(internalQueryRef: InternalQueryReference): QueryReference; + +// Warnings were encountered during analysis: +// +// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts +// src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts +// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts +// src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts + +// (No @packageDocumentation comment for this package) + +``` diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 1fa0667d5e7..9057e4f8f2a 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -3085,9 +3085,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:31:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useLoadableQuery.ts:50:5 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:29:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useLoadableQuery.ts:49:5 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.changeset/spicy-drinks-camp.md b/.changeset/spicy-drinks-camp.md new file mode 100644 index 00000000000..24c9d189945 --- /dev/null +++ b/.changeset/spicy-drinks-camp.md @@ -0,0 +1,5 @@ +--- +'@apollo/client': patch +--- + +Address bundling issue introduced in [#11412](https://github.com/apollographql/apollo-client/pull/11412) where the `react/cache` internals ended up duplicated in the bundle. This was due to the fact that we had a `react/hooks` entrypoint that imported these files along with the newly introduced `createQueryPreloader` function, which lived outside of the `react/hooks` folder. diff --git a/.size-limits.json b/.size-limits.json index dea1c9e18db..f1e85aafcac 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39137, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32653 + "dist/apollo-client.min.cjs": 39184, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32696 } diff --git a/config/entryPoints.js b/config/entryPoints.js index 3cd167c045e..896f87acf9f 100644 --- a/config/entryPoints.js +++ b/config/entryPoints.js @@ -22,6 +22,7 @@ const entryPoints = [ { dirs: ["react", "context"] }, { dirs: ["react", "hoc"] }, { dirs: ["react", "hooks"] }, + { dirs: ["react", "internal"] }, { dirs: ["react", "parser"] }, { dirs: ["react", "ssr"] }, { dirs: ["testing"], extensions: [".js", ".jsx"] }, diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index d51ea5ff2ad..9a16f9572d9 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -333,6 +333,17 @@ Array [ ] `; +exports[`exports of public entry points @apollo/client/react/internal 1`] = ` +Array [ + "InternalQueryReference", + "getSuspenseCache", + "getWrappedPromise", + "unwrapQueryRef", + "updateWrappedQueryRef", + "wrapQueryRef", +] +`; + exports[`exports of public entry points @apollo/client/react/parser 1`] = ` Array [ "DocumentType", diff --git a/src/__tests__/exports.ts b/src/__tests__/exports.ts index dc46f2498ad..181a1cc2b0a 100644 --- a/src/__tests__/exports.ts +++ b/src/__tests__/exports.ts @@ -26,6 +26,7 @@ import * as reactComponents from "../react/components"; import * as reactContext from "../react/context"; import * as reactHOC from "../react/hoc"; import * as reactHooks from "../react/hooks"; +import * as reactInternal from "../react/internal"; import * as reactParser from "../react/parser"; import * as reactSSR from "../react/ssr"; import * as testing from "../testing"; @@ -71,6 +72,7 @@ describe("exports of public entry points", () => { check("@apollo/client/react/context", reactContext); check("@apollo/client/react/hoc", reactHOC); check("@apollo/client/react/hooks", reactHooks); + check("@apollo/client/react/internal", reactInternal); check("@apollo/client/react/parser", reactParser); check("@apollo/client/react/ssr", reactSSR); check("@apollo/client/testing", testing); diff --git a/src/react/cache/index.ts b/src/react/cache/index.ts deleted file mode 100644 index 25a6d03bee5..00000000000 --- a/src/react/cache/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { SuspenseCacheOptions } from "./SuspenseCache.js"; -export { getSuspenseCache } from "./getSuspenseCache.js"; diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 75d07621029..3b1d4109261 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -44,7 +44,7 @@ import { import { useBackgroundQuery } from "../useBackgroundQuery"; import { useReadQuery } from "../useReadQuery"; import { ApolloProvider } from "../../context"; -import { QueryReference, getWrappedPromise } from "../../cache/QueryReference"; +import { QueryReference, getWrappedPromise } from "../../internal"; import { InMemoryCache } from "../../../cache"; import { SuspenseQueryHookFetchPolicy, diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index 37734d6bd20..a1ee0464f7e 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -22,7 +22,7 @@ import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; import { Suspense } from "react"; import { createQueryPreloader } from "../../query-preloader/createQueryPreloader"; import userEvent from "@testing-library/user-event"; -import { QueryReference } from "../../cache/QueryReference"; +import { QueryReference } from "../../internal"; import { useBackgroundQuery } from "../useBackgroundQuery"; import { useLoadableQuery } from "../useLoadableQuery"; import { concatPagination } from "../../../utilities"; diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 96ae008360e..af5058b5cac 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -8,19 +8,18 @@ import type { } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { + getSuspenseCache, unwrapQueryRef, updateWrappedQueryRef, wrapQueryRef, -} from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; +} from "../internal/index.js"; +import type { CacheKey, QueryReference } from "../internal/index.js"; import type { BackgroundQueryHookOptions, NoInfer } from "../types/types.js"; import { __use } from "./internal/index.js"; -import { getSuspenseCache } from "../cache/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; import type { DeepPartial } from "../../utilities/index.js"; -import type { CacheKey } from "../cache/types.js"; import type { SkipToken } from "./constants.js"; export type UseBackgroundQueryResult< diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 96f8cc974fa..3b4d9fba348 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -8,14 +8,14 @@ import type { } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { + getSuspenseCache, unwrapQueryRef, updateWrappedQueryRef, wrapQueryRef, -} from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; +} from "../internal/index.js"; +import type { CacheKey, QueryReference } from "../internal/index.js"; import type { LoadableQueryHookOptions } from "../types/types.js"; import { __use, useRenderGuard } from "./internal/index.js"; -import { getSuspenseCache } from "../cache/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; @@ -23,7 +23,6 @@ import type { DeepPartial, OnlyRequiredProperties, } from "../../utilities/index.js"; -import type { CacheKey } from "../cache/types.js"; import { invariant } from "../../utilities/globals/index.js"; export type LoadQueryFunction = ( diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts index 1e76e5cdce1..c5470e1540f 100644 --- a/src/react/hooks/useQueryRefHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -4,8 +4,8 @@ import { unwrapQueryRef, updateWrappedQueryRef, wrapQueryRef, -} from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; +} from "../internal/index.js"; +import type { QueryReference } from "../internal/index.js"; import type { OperationVariables } from "../../core/types.js"; import type { RefetchFunction, FetchMoreFunction } from "./useSuspenseQuery.js"; import type { FetchMoreQueryOptions } from "../../core/watchQueryOptions.js"; diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index f71d83b35a9..3f110b26164 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -3,8 +3,8 @@ import { getWrappedPromise, unwrapQueryRef, updateWrappedQueryRef, -} from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; +} from "../internal/index.js"; +import type { QueryReference } from "../internal/index.js"; import { __use } from "./internal/index.js"; import { toApolloError } from "./useSuspenseQuery.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index 9291aade216..3a69e175ed5 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -21,11 +21,11 @@ import type { NoInfer, } from "../types/types.js"; import { __use, useDeepMemo } from "./internal/index.js"; -import { getSuspenseCache } from "../cache/index.js"; +import { getSuspenseCache } from "../internal/index.js"; import { canonicalStringify } from "../../cache/index.js"; import { skipToken } from "./constants.js"; import type { SkipToken } from "./constants.js"; -import type { CacheKey, QueryKey } from "../cache/types.js"; +import type { CacheKey, QueryKey } from "../internal/index.js"; export interface UseSuspenseQueryResult< TData = unknown, diff --git a/src/react/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts similarity index 97% rename from src/react/cache/QueryReference.ts rename to src/react/internal/cache/QueryReference.ts index 65b11f929fc..16d5366b7e2 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -5,18 +5,18 @@ import type { ObservableQuery, OperationVariables, WatchQueryOptions, -} from "../../core/index.js"; +} from "../../../core/index.js"; import type { ObservableSubscription, PromiseWithState, -} from "../../utilities/index.js"; +} from "../../../utilities/index.js"; import { createFulfilledPromise, createRejectedPromise, -} from "../../utilities/index.js"; +} from "../../../utilities/index.js"; import type { QueryKey } from "./types.js"; -import type { useBackgroundQuery, useReadQuery } from "../hooks/index.js"; -import { wrapPromiseWithState } from "../../utilities/index.js"; +import type { useBackgroundQuery, useReadQuery } from "../../hooks/index.js"; +import { wrapPromiseWithState } from "../../../utilities/index.js"; type QueryRefPromise = PromiseWithState>; diff --git a/src/react/cache/SuspenseCache.ts b/src/react/internal/cache/SuspenseCache.ts similarity index 92% rename from src/react/cache/SuspenseCache.ts rename to src/react/internal/cache/SuspenseCache.ts index 36641c76cb4..d66eb905a5a 100644 --- a/src/react/cache/SuspenseCache.ts +++ b/src/react/internal/cache/SuspenseCache.ts @@ -1,6 +1,6 @@ import { Trie } from "@wry/trie"; -import type { ObservableQuery } from "../../core/index.js"; -import { canUseWeakMap } from "../../utilities/index.js"; +import type { ObservableQuery } from "../../../core/index.js"; +import { canUseWeakMap } from "../../../utilities/index.js"; import { InternalQueryReference } from "./QueryReference.js"; import type { CacheKey } from "./types.js"; diff --git a/src/react/cache/__tests__/QueryReference.test.ts b/src/react/internal/cache/__tests__/QueryReference.test.ts similarity index 87% rename from src/react/cache/__tests__/QueryReference.test.ts rename to src/react/internal/cache/__tests__/QueryReference.test.ts index f520a9a2e53..7e015109333 100644 --- a/src/react/cache/__tests__/QueryReference.test.ts +++ b/src/react/internal/cache/__tests__/QueryReference.test.ts @@ -3,8 +3,8 @@ import { ApolloLink, InMemoryCache, Observable, -} from "../../../core"; -import { setupSimpleCase } from "../../../testing/internal"; +} from "../../../../core"; +import { setupSimpleCase } from "../../../../testing/internal"; import { InternalQueryReference } from "../QueryReference"; test("kicks off request immediately when created", async () => { diff --git a/src/react/cache/getSuspenseCache.ts b/src/react/internal/cache/getSuspenseCache.ts similarity index 75% rename from src/react/cache/getSuspenseCache.ts rename to src/react/internal/cache/getSuspenseCache.ts index 93fbc72b98a..d9547cdc995 100644 --- a/src/react/cache/getSuspenseCache.ts +++ b/src/react/internal/cache/getSuspenseCache.ts @@ -1,8 +1,8 @@ -import type { SuspenseCacheOptions } from "./index.js"; +import type { SuspenseCacheOptions } from "../index.js"; import { SuspenseCache } from "./SuspenseCache.js"; -import type { ApolloClient } from "../../core/ApolloClient.js"; +import type { ApolloClient } from "../../../core/ApolloClient.js"; -declare module "../../core/ApolloClient.js" { +declare module "../../../core/ApolloClient.js" { interface DefaultOptions { react?: { suspense?: Readonly; diff --git a/src/react/cache/types.ts b/src/react/internal/cache/types.ts similarity index 100% rename from src/react/cache/types.ts rename to src/react/internal/cache/types.ts diff --git a/src/react/internal/index.ts b/src/react/internal/index.ts new file mode 100644 index 00000000000..cbcab8f0209 --- /dev/null +++ b/src/react/internal/index.ts @@ -0,0 +1,11 @@ +export { getSuspenseCache } from "./cache/getSuspenseCache.js"; +export type { CacheKey, QueryKey } from "./cache/types.js"; +export type { QueryReference } from "./cache/QueryReference.js"; +export { + InternalQueryReference, + getWrappedPromise, + unwrapQueryRef, + updateWrappedQueryRef, + wrapQueryRef, +} from "./cache/QueryReference.js"; +export type { SuspenseCacheOptions } from "./cache/SuspenseCache.js"; diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 38878cdfbb5..9b88fd385c9 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -17,7 +17,7 @@ import { wait, } from "../../../testing"; import { expectTypeOf } from "expect-type"; -import { QueryReference, unwrapQueryRef } from "../../cache/QueryReference"; +import { QueryReference, unwrapQueryRef } from "../../internal"; import { DeepPartial, Observable } from "../../../utilities"; import { SimpleCaseData, diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 25a1bdc2858..f5a3a03ab05 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -13,11 +13,8 @@ import type { DeepPartial, OnlyRequiredProperties, } from "../../utilities/index.js"; -import { - InternalQueryReference, - wrapQueryRef, -} from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; +import { InternalQueryReference, wrapQueryRef } from "../internal/index.js"; +import type { QueryReference } from "../internal/index.js"; import type { NoInfer } from "../index.js"; type VariablesOption = diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 1d30b6f983b..f6e24f33474 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -26,7 +26,7 @@ import type { /* QueryReference type */ -export type { QueryReference } from "../cache/QueryReference.js"; +export type { QueryReference } from "../internal/index.js"; /* Common types */ diff --git a/src/testing/matchers/toBeDisposed.ts b/src/testing/matchers/toBeDisposed.ts index 452cd45ef6b..b9ca3c6199c 100644 --- a/src/testing/matchers/toBeDisposed.ts +++ b/src/testing/matchers/toBeDisposed.ts @@ -1,9 +1,9 @@ import type { MatcherFunction } from "expect"; -import type { QueryReference } from "../../react/cache/QueryReference.js"; +import type { QueryReference } from "../../react/internal/index.js"; import { InternalQueryReference, unwrapQueryRef, -} from "../../react/cache/QueryReference.js"; +} from "../../react/internal/index.js"; function isQueryRef(queryRef: unknown): queryRef is QueryReference { try { diff --git a/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts b/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts index 7244952fed6..9cfdd2c1d78 100644 --- a/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts +++ b/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts @@ -3,8 +3,8 @@ import type { DocumentNode } from "graphql"; import type { OperationVariables } from "../../core/index.js"; import { ApolloClient } from "../../core/index.js"; import { canonicalStringify } from "../../cache/index.js"; -import { getSuspenseCache } from "../../react/cache/index.js"; -import type { CacheKey } from "../../react/cache/types.js"; +import { getSuspenseCache } from "../../react/internal/index.js"; +import type { CacheKey } from "../../react/internal/index.js"; export const toHaveSuspenseCacheEntryUsing: MatcherFunction< [ From 3cbb207800e0837f981f7cb6636c5e245a4db427 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 19 Dec 2023 08:35:17 -0700 Subject: [PATCH 68/90] Rewrite `useBackgroundQuery` tests to use Profiler (#11437) --- .size-limits.json | 4 +- .../__tests__/useBackgroundQuery.test.tsx | 9025 ++++++++--------- .../__tests__/useQueryRefHandlers.test.tsx | 80 +- src/testing/internal/scenarios/index.ts | 8 +- 4 files changed, 4385 insertions(+), 4732 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index f1e85aafcac..7a4493b82cf 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39184, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32696 + "dist/apollo-client.min.cjs": 39135, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32651 } diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 3b1d4109261..006cc0c876e 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -1,3069 +1,3418 @@ -import React, { ComponentProps, Fragment, StrictMode, Suspense } from "react"; -import { - act, - render, - screen, - screen as _screen, - renderHook, - RenderHookOptions, - waitFor, -} from "@testing-library/react"; +import React, { Suspense } from "react"; +import { act, screen, renderHook } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary"; +import { + ErrorBoundary as ReactErrorBoundary, + FallbackProps, +} from "react-error-boundary"; import { expectTypeOf } from "expect-type"; import { GraphQLError } from "graphql"; import { gql, ApolloError, - DocumentNode, ApolloClient, ErrorPolicy, - NormalizedCacheObject, NetworkStatus, - ApolloCache, TypedDocumentNode, ApolloLink, Observable, - FetchMoreQueryOptions, - OperationVariables, - ApolloQueryResult, } from "../../../core"; import { MockedResponse, - MockedProvider, MockLink, MockSubscriptionLink, mockSingleLink, + MockedProvider, } from "../../../testing"; import { concatPagination, offsetLimitPagination, DeepPartial, - cloneDeep, } from "../../../utilities"; import { useBackgroundQuery } from "../useBackgroundQuery"; -import { useReadQuery } from "../useReadQuery"; +import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; import { ApolloProvider } from "../../context"; -import { QueryReference, getWrappedPromise } from "../../internal"; +import { QueryReference } from "../../internal"; import { InMemoryCache } from "../../../cache"; -import { - SuspenseQueryHookFetchPolicy, - SuspenseQueryHookOptions, -} from "../../types/types"; +import { SuspenseQueryHookFetchPolicy } from "../../types/types"; import equal from "@wry/equality"; import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import { skipToken } from "../constants"; -import { profile, spyOnConsole } from "../../../testing/internal"; - -function renderIntegrationTest({ - client, -}: { - client?: ApolloClient; -} = {}) { - const query: TypedDocumentNode = gql` - query SimpleQuery { - foo { - bar - } - } - `; - - const mocks = [ - { - request: { query }, - result: { data: { foo: { bar: "hello" } } }, - }, - ]; - const _client = - client || - new ApolloClient({ - cache: new InMemoryCache(), - link: new MockLink(mocks), - }); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - }; - const errorBoundaryProps: ErrorBoundaryProps = { - fallback:
Error
, - onError: (error) => { - renders.errorCount++; - renders.errors.push(error); - }, - }; - - interface QueryData { - foo: { bar: string }; - } - +import { + PaginatedCaseData, + Profiler, + SimpleCaseData, + VariablesCaseData, + VariablesCaseVariables, + createProfiler, + renderWithClient, + renderWithMocks, + setupPaginatedCase, + setupSimpleCase, + setupVariablesCase, + spyOnConsole, + useTrackRenders, +} from "../../../testing/internal"; + +function createDefaultTrackedComponents< + Snapshot extends { result: UseReadQueryResult | null }, + TData = Snapshot["result"] extends UseReadQueryResult | null ? + TData + : unknown, +>(Profiler: Profiler) { function SuspenseFallback() { - renders.suspenseCount++; - return
loading
; + useTrackRenders(); + return
Loading
; } - function Child({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); - // count renders in the child component - renders.count++; - return
{data.foo.bar}
; - } + function ReadQueryHook({ queryRef }: { queryRef: QueryReference }) { + useTrackRenders(); + Profiler.mergeSnapshot({ + result: useReadQuery(queryRef), + } as Partial); - function Parent() { - const [queryRef] = useBackgroundQuery(query); - return ; + return null; } - function ParentWithVariables() { - const [queryRef] = useBackgroundQuery(query); - return ; + return { SuspenseFallback, ReadQueryHook }; +} + +function createTrackedErrorComponents( + Profiler: Profiler +) { + function ErrorFallback({ error }: FallbackProps) { + useTrackRenders({ name: "ErrorFallback" }); + Profiler.mergeSnapshot({ error } as Partial); + + return
Error
; } - function App({ variables }: { variables?: Record }) { + function ErrorBoundary({ children }: { children: React.ReactNode }) { return ( - - - }> - {variables ? - - : } - - - + + {children} + ); } - const { ...rest } = render(); - return { ...rest, query, client: _client, renders }; + return { ErrorBoundary }; } -interface VariablesCaseData { - character: { - id: string; - name: string; - }; +function createErrorProfiler() { + return createProfiler({ + initialSnapshot: { + error: null as Error | null, + result: null as UseReadQueryResult | null, + }, + }); } - -interface VariablesCaseVariables { - id: string; +function createDefaultProfiler() { + return createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); } -function useVariablesIntegrationTestCase() { - const query: TypedDocumentNode = - gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"]; - let mocks = [...CHARACTERS].map((name, index) => ({ - request: { query, variables: { id: String(index + 1) } }, - result: { data: { character: { id: String(index + 1), name } } }, - })); - return { mocks, query }; -} +it("fetches a simple query with minimal config", async () => { + const { query, mocks } = setupSimpleCase(); -function renderVariablesIntegrationTest({ - variables, - mocks, - errorPolicy, - options, - cache, -}: { - mocks?: { - request: { query: DocumentNode; variables: { id: string } }; - result: { - data?: { - character: { - id: string; - name: string | null; - }; - }; - }; - }[]; - variables: { id: string }; - options?: SuspenseQueryHookOptions; - cache?: InMemoryCache; - errorPolicy?: ErrorPolicy; -}) { - let { mocks: _mocks, query } = useVariablesIntegrationTestCase(); - - // duplicate mocks with (updated) in the name for refetches - _mocks = [..._mocks, ..._mocks, ..._mocks].map( - ({ request, result }, index) => { - return { - request: request, - result: { - data: { - character: { - ...result.data.character, - name: - index > 3 ? - index > 7 ? - `${result.data.character.name} (updated again)` - : `${result.data.character.name} (updated)` - : result.data.character.name, - }, - }, - }, - delay: 200, - }; - } - ); - const client = new ApolloClient({ - cache: cache || new InMemoryCache(), - link: new MockLink(mocks || _mocks), - }); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: VariablesCaseData; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + const Profiler = createDefaultProfiler(); - const errorBoundaryProps: ErrorBoundaryProps = { - fallback:
Error
, - onError: (error) => { - renders.errorCount++; - renders.errors.push(error); - }, - }; + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function SuspenseFallback() { - renders.suspenseCount++; - return
loading
; - } - - function Child({ - refetch, - variables: _variables, - queryRef, - }: { - variables: VariablesCaseVariables; - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); - const [variables, setVariables] = React.useState(_variables); - // count renders in the child component - renders.count++; - renders.frames.push({ data, networkStatus, error }); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); return ( -
- {error ? -
{error.message}
- : null} - - - {data?.character.id} - {data?.character.name} -
+ }> + + ); } - function ParentWithVariables({ - variables, - errorPolicy = "none", - }: { - variables: VariablesCaseVariables; - errorPolicy?: ErrorPolicy; - }) { - const [queryRef, { refetch }] = useBackgroundQuery(query, { - ...options, - variables, - errorPolicy, + renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - return ( - - ); } - function App({ - variables, - errorPolicy, - }: { - variables: VariablesCaseVariables; - errorPolicy?: ErrorPolicy; - }) { + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it("allows the client to be overridden", async () => { + const { query } = setupSimpleCase(); + + const globalClient = new ApolloClient({ + link: new ApolloLink(() => + Observable.of({ data: { greeting: "global hello" } }) + ), + cache: new InMemoryCache(), + }); + + const localClient = new ApolloClient({ + link: new ApolloLink(() => + Observable.of({ data: { greeting: "local hello" } }) + ), + cache: new InMemoryCache(), + }); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { client: localClient }); + return ( - - - }> - - - - + }> + + ); } - const ProfiledApp = profile>({ - Component: App, - snapshotDOM: true, - onRender: ({ replaceSnapshot }) => replaceSnapshot(cloneDeep(renders)), - }); + renderWithClient(, { client: globalClient, wrapper: Profiler }); - const { ...rest } = render( - - ); - const rerender = ({ variables }: { variables: VariablesCaseVariables }) => { - return rest.rerender(); - }; - return { - ...rest, - ProfiledApp, - query, - rerender, - client, - renders, - mocks: mocks || _mocks, - }; -} + { + const { renderedComponents } = await Profiler.takeRender(); -function renderPaginatedIntegrationTest({ - updateQuery, - fieldPolicies, -}: { - fieldPolicies?: boolean; - updateQuery?: boolean; - mocks?: { - request: { - query: DocumentNode; - variables: { offset: number; limit: number }; - }; - result: { - data: { - letters: { - letter: string; - position: number; - }[]; - }; - }; - }[]; -} = {}) { - interface QueryData { - letters: { - letter: string; - position: number; - }[]; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } - interface Variables { - limit?: number; - offset?: number; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "local hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); } - const query: TypedDocumentNode = gql` - query letters($limit: Int, $offset: Int) { - letters(limit: $limit) { - letter - position - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it("passes context to the link", async () => { + const query = gql` + query ContextQuery { + context } `; - const data = "ABCDEFG" - .split("") - .map((letter, index) => ({ letter, position: index + 1 })); - const link = new ApolloLink((operation) => { - const { offset = 0, limit = 2 } = operation.variables; - const letters = data.slice(offset, offset + limit); - return new Observable((observer) => { - setTimeout(() => { - observer.next({ data: { letters } }); - observer.complete(); - }, 10); + const { valueA, valueB } = operation.getContext(); + + observer.next({ data: { context: { valueA, valueB } } }); + observer.complete(); }); }); - const cacheWithTypePolicies = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - letters: concatPagination(), - }, - }, - }, - }); - const client = new ApolloClient({ - cache: fieldPolicies ? cacheWithTypePolicies : new InMemoryCache(), - link, - }); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - } + const Profiler = createDefaultProfiler(); - function SuspenseFallback() { - ProfiledApp.mergeSnapshot(({ suspenseCount }) => ({ - suspenseCount: suspenseCount + 1, - })); - return
loading
; - } - - function Child({ - queryRef, - onFetchMore, - }: { - onFetchMore: (options: FetchMoreQueryOptions) => void; - queryRef: QueryReference; - }) { - const { data, error } = useReadQuery(queryRef); - // count renders in the child component - ProfiledApp.mergeSnapshot(({ count }) => ({ - count: count + 1, - })); - return ( -
- {error ? -
{error.message}
- : null} - -
    - {data.letters.map(({ letter, position }) => ( -
  • - {letter} -
  • - ))} -
-
+ function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + context: { valueA: "A", valueB: "B" }, + }); + + return ( + }> + + ); } - function ParentWithVariables() { - const [queryRef, { fetchMore }] = useBackgroundQuery(query, { - variables: { limit: 2, offset: 0 }, + renderWithMocks(, { link, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { context: { valueA: "A", valueB: "B" } }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - return ; } +}); - function App() { - return ( - - Error} - onError={(error) => { - ProfiledApp.mergeSnapshot(({ errorCount, errors }) => ({ - errorCount: errorCount + 1, - errors: errors.concat(error), - })); - }} - > - }> - - - - - ); +it('enables canonical results when canonizeResults is "true"', async () => { + interface Result { + __typename: string; + value: number; } - const ProfiledApp = profile({ - Component: App, - snapshotDOM: true, - initialSnapshot: { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - } as Renders, + interface Data { + results: Result[]; + } + + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, }); - const { ...rest } = render(); - return { ...rest, ProfiledApp, data, query, client }; -} -type RenderSuspenseHookOptions = Omit< - RenderHookOptions, - "wrapper" -> & { - client?: ApolloClient; - link?: ApolloLink; - cache?: ApolloCache; - mocks?: MockedResponse[]; - strictMode?: boolean; -}; - -interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: Result[]; -} + const query: TypedDocumentNode = gql` + query { + results { + value + } + } + `; -interface SimpleQueryData { - greeting: string; -} + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; -function renderSuspenseHook( - render: (initialProps: Props) => Result, - options: RenderSuspenseHookOptions = Object.create(null) -) { - function SuspenseFallback() { - renders.suspenseCount++; + cache.writeQuery({ query, data: { results } }); - return
loading
; - } + const Profiler = createDefaultProfiler(); - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const { mocks = [], strictMode, ...renderHookOptions } = options; + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { canonizeResults: true }); - const client = - options.client || - new ApolloClient({ - cache: options.cache || new InMemoryCache(), - link: options.link || new MockLink(mocks), - }); + return ( + }> + + + ); + } - const view = renderHook( - (props) => { - renders.count++; + renderWithMocks(, { cache, wrapper: Profiler }); - const view = render(props); + const { + snapshot: { result }, + } = await Profiler.takeRender(); - renders.frames.push(view); + const resultSet = new Set(result!.data.results); + const values = Array.from(resultSet).map((item) => item.value); - return view; - }, - { - ...renderHookOptions, - wrapper: ({ children }) => { - const Wrapper = strictMode ? StrictMode : Fragment; - - return ( - - }> - Error} - onError={(error) => { - renders.errorCount++; - renders.errors.push(error); - }} - > - {children} - - - - ); - }, - } - ); + expect(result!.data).toEqual({ results }); + expect(result!.data.results.length).toBe(6); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); +}); - return { ...view, renders }; -} +it("can disable canonical results when the cache's canonizeResults setting is true", async () => { + interface Result { + __typename: string; + value: number; + } -describe("useBackgroundQuery", () => { - it("fetches a simple query with minimal config", async () => { - const query = gql` - query { - hello - } - `; - const mocks = [ - { - request: { query }, - result: { data: { hello: "world 1" } }, + interface Data { + results: Result[]; + } + + const cache = new InMemoryCache({ + canonizeResults: true, + typePolicies: { + Result: { + keyFields: false, }, - ]; - const { result } = renderHook(() => useBackgroundQuery(query), { - wrapper: ({ children }) => ( - {children} - ), - }); + }, + }); - const [queryRef] = result.current; + const query: TypedDocumentNode = gql` + query { + results { + value + } + } + `; - const _result = await getWrappedPromise(queryRef); + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; - expect(_result).toEqual({ - data: { hello: "world 1" }, - loading: false, - networkStatus: 7, - }); - }); + cache.writeQuery({ query, data: { results } }); - it("allows the client to be overridden", async () => { - const query: TypedDocumentNode = gql` - query UserQuery { - greeting - } - `; + const Profiler = createDefaultProfiler(); - const globalClient = new ApolloClient({ - link: new ApolloLink(() => - Observable.of({ data: { greeting: "global hello" } }) - ), - cache: new InMemoryCache(), - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const localClient = new ApolloClient({ - link: new ApolloLink(() => - Observable.of({ data: { greeting: "local hello" } }) - ), - cache: new InMemoryCache(), - }); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { canonizeResults: false }); - const { result } = renderSuspenseHook( - () => useBackgroundQuery(query, { client: localClient }), - { client: globalClient } + return ( + }> + + ); + } - const [queryRef] = result.current; + renderWithMocks(, { cache, wrapper: Profiler }); - const _result = await getWrappedPromise(queryRef); + const { snapshot } = await Profiler.takeRender(); + const result = snapshot.result!; - await waitFor(() => { - expect(_result).toEqual({ - data: { greeting: "local hello" }, - loading: false, - networkStatus: NetworkStatus.ready, - }); - }); + const resultSet = new Set(result.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(result.data).toEqual({ results }); + expect(result.data.results.length).toBe(6); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); +}); + +it("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { + const { query } = setupSimpleCase(); + const cache = new InMemoryCache(); + const link = mockSingleLink({ + request: { query }, + result: { data: { greeting: "from link" } }, + delay: 20, }); - it("passes context to the link", async () => { - const query = gql` - query ContextQuery { - context - } - `; + const client = new ApolloClient({ link, cache }); - const link = new ApolloLink((operation) => { - return new Observable((observer) => { - const { valueA, valueB } = operation.getContext(); + cache.writeQuery({ query, data: { greeting: "from cache" } }); - observer.next({ data: { context: { valueA, valueB } } }); - observer.complete(); - }); + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-and-network", }); - const { result } = renderHook( - () => - useBackgroundQuery(query, { - context: { valueA: "A", valueB: "B" }, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } + return ( + }> + + ); + } - const [queryRef] = result.current; + renderWithClient(, { client, wrapper: Profiler }); - const _result = await getWrappedPromise(queryRef); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - await waitFor(() => { - expect(_result).toMatchObject({ - data: { context: { valueA: "A", valueB: "B" } }, - networkStatus: NetworkStatus.ready, - }); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "from cache" }, + error: undefined, + networkStatus: NetworkStatus.loading, }); - }); + } - it('enables canonical results when canonizeResults is "true"', async () => { - interface Result { - __typename: string; - value: number; - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const cache = new InMemoryCache({ - typePolicies: { - Result: { - keyFields: false, - }, - }, + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const query: TypedDocumentNode<{ results: Result[] }> = gql` - query { - results { - value - } - } - `; + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const results: Result[] = [ - { __typename: "Result", value: 0 }, - { __typename: "Result", value: 1 }, - { __typename: "Result", value: 1 }, - { __typename: "Result", value: 2 }, - { __typename: "Result", value: 3 }, - { __typename: "Result", value: 5 }, - ]; +it("all data is present in the cache, no network request is made", async () => { + const { query } = setupSimpleCase(); + const cache = new InMemoryCache(); - cache.writeQuery({ - query, - data: { results }, + let fetchCount = 0; + const link = new ApolloLink((operation) => { + fetchCount++; + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { greeting: "from link" } }); + observer.complete(); + }, 20); + }); + }); + + const client = new ApolloClient({ link, cache }); + + cache.writeQuery({ query, data: { greeting: "from cache" } }); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", }); - const { result } = renderHook( - () => - useBackgroundQuery(query, { - canonizeResults: true, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } + return ( + }> + + ); + } - const [queryRef] = result.current; + renderWithClient(, { client, wrapper: Profiler }); - const _result = await getWrappedPromise(queryRef); - const resultSet = new Set(_result.data.results); - const values = Array.from(resultSet).map((item) => item.value); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(_result.data).toEqual({ results }); - expect(_result.data.results.length).toBe(6); - expect(resultSet.size).toBe(5); - expect(values).toEqual([0, 1, 2, 3, 5]); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "from cache" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - it("can disable canonical results when the cache's canonizeResults setting is true", async () => { - interface Result { - __typename: string; - value: number; + expect(fetchCount).toBe(0); + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it("partial data is present in the cache so it is ignored and network request is made", async () => { + const query = gql` + { + hello + foo } + `; + const cache = new InMemoryCache(); + const link = mockSingleLink({ + request: { query }, + result: { data: { hello: "from link", foo: "bar" } }, + delay: 20, + }); - const cache = new InMemoryCache({ - canonizeResults: true, - typePolicies: { - Result: { - keyFields: false, - }, - }, - }); + const client = new ApolloClient({ link, cache }); - const query: TypedDocumentNode<{ results: Result[] }> = gql` - query { - results { - value - } - } - `; + { + // we expect a "Missing field 'foo' while writing result..." error + // when writing hello to the cache, so we'll silence the console.error + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ query, data: { hello: "from cache" } }); + } - const results: Result[] = [ - { __typename: "Result", value: 0 }, - { __typename: "Result", value: 1 }, - { __typename: "Result", value: 1 }, - { __typename: "Result", value: 2 }, - { __typename: "Result", value: 3 }, - { __typename: "Result", value: 5 }, - ]; + const Profiler = createDefaultProfiler(); - cache.writeQuery({ - query, - data: { results }, + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", }); - const { result } = renderHook( - () => - useBackgroundQuery(query, { - canonizeResults: false, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } + return ( + }> + + ); + } - const [queryRef] = result.current; + renderWithClient(, { client, wrapper: Profiler }); - const _result = await getWrappedPromise(queryRef); - const resultSet = new Set(_result.data.results); - const values = Array.from(resultSet).map((item) => item.value); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(_result.data).toEqual({ results }); - expect(_result.data.results.length).toBe(6); - expect(resultSet.size).toBe(6); - expect(values).toEqual([0, 1, 1, 2, 3, 5]); - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - // TODO(FIXME): test fails, should return cache data first if it exists - it.skip("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { - const query = gql` - { - hello - } - `; - const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link" } }, - delay: 20, - }); + { + const { snapshot } = await Profiler.takeRender(); - const client = new ApolloClient({ - link, - cache, + expect(snapshot.result).toEqual({ + data: { foo: "bar", hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - cache.writeQuery({ query, data: { hello: "from cache" } }); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const { result } = renderHook( - () => useBackgroundQuery(query, { fetchPolicy: "cache-and-network" }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); +it("existing data in the cache is ignored when fetchPolicy is 'network-only'", async () => { + const { query } = setupSimpleCase(); + const cache = new InMemoryCache(); + const link = mockSingleLink({ + request: { query }, + result: { data: { greeting: "from link" } }, + delay: 20, + }); - const [queryRef] = result.current; + const client = new ApolloClient({ link, cache }); - const _result = await getWrappedPromise(queryRef); + cache.writeQuery({ query, data: { greeting: "from cache" } }); - expect(_result).toEqual({ - data: { hello: "from link" }, - loading: false, - networkStatus: 7, - }); - }); + const Profiler = createDefaultProfiler(); - it("all data is present in the cache, no network request is made", async () => { - const query = gql` - { - hello - } - `; - const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link" } }, - delay: 20, - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const client = new ApolloClient({ - link, - cache, + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "network-only", }); - cache.writeQuery({ query, data: { hello: "from cache" } }); - - const { result } = renderHook( - () => useBackgroundQuery(query, { fetchPolicy: "cache-first" }), - { - wrapper: ({ children }) => ( - {children} - ), - } + return ( + }> + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); - const [queryRef] = result.current; + { + const { renderedComponents } = await Profiler.takeRender(); - const _result = await getWrappedPromise(queryRef); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); - expect(_result).toEqual({ - data: { hello: "from cache" }, - loading: false, - networkStatus: 7, + expect(snapshot.result).toEqual({ + data: { greeting: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } + + expect(client.cache.extract()).toEqual({ + ROOT_QUERY: { __typename: "Query", greeting: "from link" }, }); - it("partial data is present in the cache so it is ignored and network request is made", async () => { - const query = gql` - { - hello - foo - } - `; - const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link", foo: "bar" } }, - delay: 20, - }); - const client = new ApolloClient({ - link, - cache, - }); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - // we expect a "Missing field 'foo' while writing result..." error - // when writing hello to the cache, so we'll silence the console.error - const originalConsoleError = console.error; - console.error = () => { - /* noop */ - }; - cache.writeQuery({ query, data: { hello: "from cache" } }); - console.error = originalConsoleError; +it("fetches data from the network but does not update the cache when fetchPolicy is 'no-cache'", async () => { + const { query } = setupSimpleCase(); + const cache = new InMemoryCache(); + const link = mockSingleLink({ + request: { query }, + result: { data: { greeting: "from link" } }, + delay: 20, + }); - const { result } = renderHook( - () => useBackgroundQuery(query, { fetchPolicy: "cache-first" }), - { - wrapper: ({ children }) => ( - {children} - ), - } + const client = new ApolloClient({ link, cache }); + + cache.writeQuery({ query, data: { greeting: "from cache" } }); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { fetchPolicy: "no-cache" }); + + return ( + }> + + ); + } - const [queryRef] = result.current; + renderWithClient(, { client, wrapper: Profiler }); - const _result = await getWrappedPromise(queryRef); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(_result).toEqual({ - data: { foo: "bar", hello: "from link" }, - loading: false, - networkStatus: 7, + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } + + expect(client.cache.extract()).toEqual({ + ROOT_QUERY: { __typename: "Query", greeting: "from cache" }, }); - it("existing data in the cache is ignored", async () => { - const query = gql` - { - hello - } - `; - const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link" } }, - delay: 20, - }); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const client = new ApolloClient({ - link, - cache, - }); +it("works with startTransition to change variables", async () => { + type Variables = { + id: string; + }; - cache.writeQuery({ query, data: { hello: "from cache" } }); + interface Data { + todo: { + id: string; + name: string; + completed: boolean; + }; + } + const user = userEvent.setup(); - const { result } = renderHook( - () => useBackgroundQuery(query, { fetchPolicy: "network-only" }), - { - wrapper: ({ children }) => ( - {children} - ), + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed } - ); + } + `; - const [queryRef] = result.current; + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + todo: { id: "2", name: "Take out trash", completed: true }, + }, + }, + delay: 10, + }, + ]; - const _result = await getWrappedPromise(queryRef); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); - expect(_result).toEqual({ - data: { hello: "from link" }, - loading: false, - networkStatus: 7, - }); - expect(client.cache.extract()).toEqual({ - ROOT_QUERY: { __typename: "Query", hello: "from link" }, - }); + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, }); - it("fetches data from the network but does not update the cache", async () => { - const query = gql` - { - hello - } - `; - const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link" } }, - delay: 20, - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const client = new ApolloClient({ - link, - cache, + function App() { + useTrackRenders(); + const [id, setId] = React.useState("1"); + const [isPending, startTransition] = React.useTransition(); + const [queryRef] = useBackgroundQuery(query, { + variables: { id }, }); - cache.writeQuery({ query, data: { hello: "from cache" } }); + Profiler.mergeSnapshot({ isPending }); - const { result } = renderHook( - () => useBackgroundQuery(query, { fetchPolicy: "no-cache" }), - { - wrapper: ({ children }) => ( - {children} - ), - } + return ( + <> + + + }> + + + + ); + } - const [queryRef] = result.current; + renderWithClient(, { client, wrapper: Profiler }); - const _result = await getWrappedPromise(queryRef); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(_result).toEqual({ - data: { hello: "from link" }, - loading: false, - networkStatus: 7, - }); - // ...but not updated in the cache - expect(client.cache.extract()).toEqual({ - ROOT_QUERY: { __typename: "Query", hello: "from cache" }, + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, }); - }); + } + + await act(() => user.click(screen.getByText("Change todo"))); - describe("integration tests with useReadQuery", () => { - it("suspends and renders hello", async () => { - const { renders } = renderIntegrationTest(); - // ensure the hook suspends immediately - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("hello")).toBeInTheDocument(); - expect(renders.count).toBe(1); + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: true, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, }); + } - it("works with startTransition to change variables", async () => { - type Variables = { - id: string; - }; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); + // Eventually we should see the updated todo content once its done + // suspending. + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "2", name: "Take out trash", completed: true } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } +}); + +it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { name - completed } } - `; + } + } + `; - const mocks: MockedResponse[] = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: false } }, - }, - delay: 10, - }, - { - request: { query, variables: { id: "2" } }, - result: { - data: { - todo: { id: "2", name: "Take out trash", completed: true }, - }, - }, - delay: 10, - }, - ]; + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + const client = new ApolloClient({ cache, link }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + const Profiler = createDefaultProfiler(); - function App() { - return ( - - }> - - - - ); - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function SuspenseFallback() { - return

Loading

; - } + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-and-network", + }); - function Parent() { - const [id, setId] = React.useState("1"); - const [queryRef] = useBackgroundQuery(query, { - variables: { id }, - }); - return ; - } + return ( + }> + + + ); + } - function Todo({ - queryRef, - onChange, - }: { - queryRef: QueryReference; - onChange: (id: string) => void; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todo } = data; - - return ( - <> - -
- {todo.name} - {todo.completed && " (completed)"} -
- - ); - } + renderWithClient(, { client, wrapper: Profiler }); - render(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } - expect(await screen.findByTestId("todo")).toBeInTheDocument(); + link.simulateResult({ + result: { + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + hasNext: true, + }, + }); - const todo = screen.getByTestId("todo"); - const button = screen.getByText("Refresh"); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(todo).toHaveTextContent("Clean room"); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - await act(() => user.click(button)); + link.simulateResult({ + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }); - // startTransition will avoid rendering the suspense fallback for already - // revealed content if the state update inside the transition causes the - // component to suspend. - // - // Here we should not see the suspense fallback while the component suspends - // until the todo is finished loading. Seeing the suspense fallback is an - // indication that we are suspending the component too late in the process. - expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - // We can ensure this works with isPending from useTransition in the process - expect(todo).toHaveAttribute("aria-busy", "true"); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - // Ensure we are showing the stale UI until the new todo has loaded - expect(todo).toHaveTextContent("Clean room"); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - // Eventually we should see the updated todo content once its done - // suspending. - await waitFor(() => { - expect(todo).toHaveTextContent("Take out trash (completed)"); - }); - }); +it("reacts to cache updates", async () => { + const { query, mocks } = setupSimpleCase(); - it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { - interface Data { - greeting: { - __typename: string; - message: string; - recipient: { name: string; __typename: string }; - }; - } + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); - const query: TypedDocumentNode = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; + const Profiler = createDefaultProfiler(); - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - const client = new ApolloClient({ cache, link }); - let renders = 0; - let suspenseCount = 0; - - function App() { - return ( - - }> - - - - ); - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function SuspenseFallback() { - suspenseCount++; - return

Loading

; - } + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); - function Parent() { - const [queryRef] = useBackgroundQuery(query, { - fetchPolicy: "cache-and-network", - }); - return ; - } + return ( + }> + + + ); + } - function Todo({ queryRef }: { queryRef: QueryReference }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - const { greeting } = data; - renders++; - - return ( - <> -
Message: {greeting.message}
-
Recipient: {greeting.recipient.name}
-
Network status: {networkStatus}
-
Error: {error ? error.message : "none"}
- - ); - } + renderWithClient(, { client, wrapper: Profiler }); - render(); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello cached" - ); - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Cached Alice" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 1" // loading - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - link.simulateResult({ - result: { - data: { - greeting: { __typename: "Greeting", message: "Hello world" }, - }, - hasNext: true, - }, - }); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello world" - ); - }); - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Cached Alice" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 7" // ready - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - link.simulateResult({ - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }); + client.writeQuery({ + query, + data: { greeting: "Hello again" }, + }); - await waitFor(() => { - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Alice" - ); - }); - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello world" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 7" // ready - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renders).toBe(3); - expect(suspenseCount).toBe(0); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } + + client.writeQuery({ + query, + data: { greeting: "You again?" }, }); - it("reacts to cache updates", async () => { - const { renders, client, query } = renderIntegrationTest(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "You again?" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("hello")).toBeInTheDocument(); - expect(renders.count).toBe(1); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - client.writeQuery({ - query, - data: { foo: { bar: "baz" } }, - }); +it("reacts to variables updates", async () => { + const { query, mocks } = setupVariablesCase(); - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("baz")).toBeInTheDocument(); + const Profiler = createDefaultProfiler(); - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(1); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - client.writeQuery({ - query, - data: { foo: { bar: "bat" } }, - }); + function App({ id }: { id: string }) { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { variables: { id } }); - expect(await screen.findByText("bat")).toBeInTheDocument(); + return ( + }> + + + ); + } - expect(renders.suspenseCount).toBe(1); + const { rerender } = renderWithMocks(, { + mocks, + wrapper: Profiler, }); - it("reacts to variables updates", async () => { - const { renders, rerender } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + { + const { snapshot } = await Profiler.takeRender(); - rerender({ variables: { id: "2" } }); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - expect(renders.suspenseCount).toBe(2); - expect(screen.getByText("loading")).toBeInTheDocument(); + rerender(); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - it("does not suspend when `skip` is true", async () => { - interface Data { - greeting: string; - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + { + const { snapshot } = await Profiler.takeRender(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "2", name: "Black Widow" }, }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
Loading...
; - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - function Parent() { - const [queryRef] = useBackgroundQuery(query, { skip: true }); +it("does not suspend when `skip` is true", async () => { + const { query, mocks } = setupSimpleCase(); - return ( - }> - {queryRef && } - - ); - } + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { skip: true }); - return
{data.greeting}
; - } + return ( + }> + {queryRef && } + + ); + } - function App() { - return ( - - - - ); - } + renderWithMocks(, { mocks, wrapper: Profiler }); - render(); + const { renderedComponents } = await Profiler.takeRender(); - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); - }); + expect(renderedComponents).toStrictEqual([App]); - it("does not suspend when using `skipToken` in options", async () => { - interface Data { - greeting: string; - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; +it("does not suspend when using `skipToken` in options", async () => { + const { query, mocks } = setupSimpleCase(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, skipToken); - function SuspenseFallback() { - return
Loading...
; - } + return ( + }> + {queryRef && } + + ); + } - function Parent() { - const [queryRef] = useBackgroundQuery(query, skipToken); + renderWithMocks(, { mocks, wrapper: Profiler }); - return ( + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it("suspends when `skip` becomes `false` after it was `true`", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [queryRef] = useBackgroundQuery(query, { skip }); + + return ( + <> + }> - {queryRef && } + {queryRef && } - ); - } + + ); + } - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + renderWithMocks(, { mocks, wrapper: Profiler }); - return
{data.greeting}
; - } + { + const { renderedComponents } = await Profiler.takeRender(); - function App() { - return ( - - - - ); - } + expect(renderedComponents).toStrictEqual([App]); + } - render(); + await act(() => user.click(screen.getByText("Run query"))); - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - it("suspends when `skip` becomes `false` after it was `true`", async () => { - interface Data { - greeting: string; - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const user = userEvent.setup(); + { + const { snapshot } = await Profiler.takeRender(); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); +it("suspends when switching away from `skipToken` in options", async () => { + const { query, mocks } = setupSimpleCase(); - function SuspenseFallback() { - return
Loading...
; - } + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery(query, { skip }); + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [queryRef] = useBackgroundQuery(query, skip ? skipToken : undefined); - return ( - <> - - }> - {queryRef && } - - - ); - } + return ( + <> + + }> + {queryRef && } + + + ); + } - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + renderWithMocks(, { mocks, wrapper: Profiler }); - return
{data.greeting}
; - } + { + const { renderedComponents } = await Profiler.takeRender(); - function App() { - return ( - - - - ); - } + expect(renderedComponents).toStrictEqual([App]); + } - render(); + await act(() => user.click(screen.getByText("Run query"))); - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); + { + const { renderedComponents } = await Profiler.takeRender(); - await act(() => user.click(screen.getByText("Run query"))); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(screen.getByText("Loading...")).toBeInTheDocument(); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - }); + } - it("suspends when switching away from `skipToken` in options", async () => { - interface Data { - greeting: string; - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const user = userEvent.setup(); +it("renders skip result, does not suspend, and maintains `data` when `skip` becomes `true` after it was `false`", async () => { + const { query, mocks } = setupSimpleCase(); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(false); + const [queryRef] = useBackgroundQuery(query, { skip }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + return ( + <> + + }> + {queryRef && } + + + ); + } + + renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
Loading...
; - } + await act(() => user.click(screen.getByText("Toggle skip"))); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : undefined - ); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - return ( - <> - - }> - {queryRef && } - - - ); - } + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - return
{data.greeting}
; - } +it("renders skip result, does not suspend, and maintains `data` when switching back to `skipToken`", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function App() { - return ( - - - - ); - } + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(false); + const [queryRef] = useBackgroundQuery(query, skip ? skipToken : undefined); + + return ( + <> + + }> + {queryRef && } + + + ); + } - render(); + renderWithMocks(, { mocks, wrapper: Profiler }); - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); + { + const { renderedComponents } = await Profiler.takeRender(); - await act(() => user.click(screen.getByText("Run query"))); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(screen.getByText("Loading...")).toBeInTheDocument(); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - }); + } - it("renders skip result, does not suspend, and maintains `data` when `skip` becomes `true` after it was `false`", async () => { - interface Data { - greeting: string; - } + await act(() => user.click(screen.getByText("Toggle skip"))); - const user = userEvent.setup(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); +it("does not make network requests when `skip` is `true`", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); - function SuspenseFallback() { - return
Loading...
; - } + let fetchCount = 0; - function Parent() { - const [skip, setSkip] = React.useState(false); - const [queryRef] = useBackgroundQuery(query, { skip }); + const link = new ApolloLink((operation) => { + return new Observable((observer) => { + fetchCount++; - return ( - <> - - }> - {queryRef && } - - + const mock = mocks.find(({ request }) => + equal(request.query, operation.query) ); - } - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + if (!mock) { + throw new Error("Could not find mock for operation"); + } - return
{data.greeting}
; - } + observer.next((mock as any).result); + observer.complete(); + }); + }); - function App() { - return ( - - - - ); - } + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - render(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect(screen.getByText("Loading...")).toBeInTheDocument(); + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [queryRef] = useBackgroundQuery(query, { skip }); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); - }); + return ( + <> + + }> + {queryRef && } + + + ); + } - await act(() => user.click(screen.getByText("Toggle skip"))); + renderWithClient(, { client, wrapper: Profiler }); - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); - }); + // initial skipped result + await Profiler.takeRender(); + expect(fetchCount).toBe(0); - it("renders skip result, does not suspend, and maintains `data` when switching back to `skipToken`", async () => { - interface Data { - greeting: string; - } + // Toggle skip to `false` + await act(() => user.click(screen.getByText("Toggle skip"))); - const user = userEvent.setup(); + expect(fetchCount).toBe(1); + { + const { renderedComponents } = await Profiler.takeRender(); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + { + const { snapshot } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
Loading...
; - } + // Toggle skip to `true` + await act(() => user.click(screen.getByText("Toggle skip"))); - function Parent() { - const [skip, setSkip] = React.useState(false); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : undefined - ); + expect(fetchCount).toBe(1); + { + const { snapshot } = await Profiler.takeRender(); - return ( - <> - - }> - {queryRef && } - - - ); - } + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); +it("does not make network requests when `skipToken` is used", async () => { + const { query, mocks } = setupSimpleCase(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const user = userEvent.setup(); - return
{data.greeting}
; - } + let fetchCount = 0; - function App() { - return ( - - - - ); - } + const link = new ApolloLink((operation) => { + return new Observable((observer) => { + fetchCount++; - render(); + const mock = mocks.find(({ request }) => + equal(request.query, operation.query) + ); - expect(screen.getByText("Loading...")).toBeInTheDocument(); + if (!mock) { + throw new Error("Could not find mock for operation"); + } - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + observer.next((mock as any).result); + observer.complete(); }); - - await act(() => user.click(screen.getByText("Toggle skip"))); - - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); }); - it("does not make network requests when `skip` is `true`", async () => { - interface Data { - greeting: string; - } + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - const user = userEvent.setup(); + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [queryRef] = useBackgroundQuery(query, skip ? skipToken : undefined); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + return ( + <> + + }> + {queryRef && } + + + ); + } - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + renderWithClient(, { client, wrapper: Profiler }); - let fetchCount = 0; + // initial skipped result + await Profiler.takeRender(); + expect(fetchCount).toBe(0); - const link = new ApolloLink((operation) => { - return new Observable((observer) => { - fetchCount++; + // Toggle skip to `false` + await act(() => user.click(screen.getByText("Toggle skip"))); - const mock = mocks.find(({ request }) => - equal(request.query, operation.query) - ); + expect(fetchCount).toBe(1); + { + const { renderedComponents } = await Profiler.takeRender(); - if (!mock) { - throw new Error("Could not find mock for operation"); - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - observer.next(mock.result); - observer.complete(); - }); - }); + { + const { snapshot } = await Profiler.takeRender(); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
Loading...
; - } + // Toggle skip to `true` + await act(() => user.click(screen.getByText("Toggle skip"))); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery(query, { skip }); + expect(fetchCount).toBe(1); + { + const { snapshot } = await Profiler.takeRender(); - return ( - <> - - }> - {queryRef && } - - - ); - } + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); +it("result is referentially stable", async () => { + const { query, mocks } = setupSimpleCase(); - return
{data.greeting}
; - } + let result: UseReadQueryResult | null = null; - function App() { - return ( - - - - ); - } + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - render(); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); - expect(fetchCount).toBe(0); + return ( + }> + + + ); + } - // Toggle skip to `false` - await act(() => user.click(screen.getByText("Toggle skip"))); + const { rerender } = renderWithMocks(, { mocks, wrapper: Profiler }); - expect(fetchCount).toBe(1); + { + const { renderedComponents } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - // Toggle skip to `true` - await act(() => user.click(screen.getByText("Toggle skip"))); + { + const { snapshot } = await Profiler.takeRender(); - expect(fetchCount).toBe(1); - }); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); - it("does not make network requests when `skipToken` is used", async () => { - interface Data { - greeting: string; - } + result = snapshot.result; + } - const user = userEvent.setup(); + rerender(); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + { + const { snapshot } = await Profiler.takeRender(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + expect(snapshot.result).toBe(result); + } +}); - let fetchCount = 0; +it("`skip` option works with `startTransition`", async () => { + const { query, mocks } = setupSimpleCase(); - const link = new ApolloLink((operation) => { - return new Observable((observer) => { - fetchCount++; + const user = userEvent.setup(); + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const mock = mocks.find(({ request }) => - equal(request.query, operation.query) - ); + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [isPending, startTransition] = React.useTransition(); + const [queryRef] = useBackgroundQuery(query, { skip }); - if (!mock) { - throw new Error("Could not find mock for operation"); - } + Profiler.mergeSnapshot({ isPending }); - observer.next(mock.result); - observer.complete(); - }); - }); + return ( + <> + + }> + {queryRef && } + + + ); + } - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + renderWithMocks(, { mocks, wrapper: Profiler }); - function SuspenseFallback() { - return
Loading...
; - } + { + const { renderedComponents } = await Profiler.takeRender(); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : undefined - ); + expect(renderedComponents).toStrictEqual([App]); + } - return ( - <> - - }> - {queryRef && } - - - ); - } + // Toggle skip to `false` + await act(() => user.click(screen.getByText("Toggle skip"))); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - return
{data.greeting}
; - } + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot).toEqual({ + isPending: true, + result: null, + }); + } - function App() { - return ( - - - - ); - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - render(); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } - expect(fetchCount).toBe(0); + await expect(Profiler).not.toRerender(); +}); - // Toggle skip to `false` - await act(() => user.click(screen.getByText("Toggle skip"))); +it("`skipToken` works with `startTransition`", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); - expect(fetchCount).toBe(1); + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - // Toggle skip to `true` - await act(() => user.click(screen.getByText("Toggle skip"))); + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [isPending, startTransition] = React.useTransition(); + const [queryRef] = useBackgroundQuery(query, skip ? skipToken : undefined); - expect(fetchCount).toBe(1); - }); + Profiler.mergeSnapshot({ isPending }); - it("`skip` result is referentially stable", async () => { - interface Data { - greeting: string; - } + return ( + <> + + }> + {queryRef && } + + + ); + } - interface CurrentResult { - current: Data | undefined; - } + renderWithMocks(, { mocks, wrapper: Profiler }); - const user = userEvent.setup(); + { + const { renderedComponents } = await Profiler.takeRender(); - const result: CurrentResult = { - current: undefined, - }; + expect(renderedComponents).toStrictEqual([App]); + } - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + // Toggle skip to `false` + await act(() => user.click(screen.getByText("Toggle skip"))); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot).toEqual({ + isPending: true, + result: null, }); + } - function SuspenseFallback() { - return
Loading...
; - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery(query, { skip }); - - return ( - <> - - }> - {queryRef && } - - - ); - } - - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } - result.current = data; + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - return
{data.greeting}
; - } +it("applies `errorPolicy` on next fetch when it changes between renders", async () => { + const { query } = setupSimpleCase(); + const user = userEvent.setup(); - function App() { - return ( - - - - ); - } + const mocks = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + delay: 10, + }, + { + request: { query }, + result: { + errors: [new GraphQLError("oops")], + }, + delay: 10, + }, + ]; - const { rerender } = render(); + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); - const skipResult = result.current; + function App() { + useTrackRenders(); + const [errorPolicy, setErrorPolicy] = React.useState("none"); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + errorPolicy, + }); - rerender(); + return ( + <> + + + }> + + + + + + ); + } - expect(result.current).toBe(skipResult); + renderWithMocks(, { mocks, wrapper: Profiler }); - // Toggle skip to `false` - await act(() => user.click(screen.getByText("Toggle skip"))); + // initial render + await Profiler.takeRender(); - expect(screen.getByText("Loading...")).toBeInTheDocument(); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const fetchedResult = result.current; - - rerender(); - - expect(result.current).toBe(fetchedResult); - }); + await act(() => user.click(screen.getByText("Change error policy"))); + await Profiler.takeRender(); - it("`skip` result is referentially stable when using `skipToken`", async () => { - interface Data { - greeting: string; - } + await act(() => user.click(screen.getByText("Refetch greeting"))); + await Profiler.takeRender(); - interface CurrentResult { - current: Data | undefined; - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const user = userEvent.setup(); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + error: null, + result: { + data: { greeting: "Hello" }, + error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), + networkStatus: NetworkStatus.error, + }, + }); + } +}); - const result: CurrentResult = { - current: undefined, - }; +it("applies `context` on next fetch when it changes between renders", async () => { + interface Data { + context: Record; + } - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + const user = userEvent.setup(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + const query: TypedDocumentNode = gql` + query { + context + } + `; - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + const link = new ApolloLink((operation) => { + return new Observable((observer) => { + setTimeout(() => { + const { phase } = operation.getContext(); + observer.next({ data: { context: { phase } } }); + observer.complete(); + }, 10); }); + }); - function SuspenseFallback() { - return
Loading...
; - } - - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : undefined - ); + const client = new ApolloClient({ link, cache: new InMemoryCache() }); - return ( - <> - - }> - {queryRef && } - - - ); - } + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + function App() { + useTrackRenders(); + const [phase, setPhase] = React.useState("initial"); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + context: { phase }, + }); - result.current = data; + return ( + <> + + + }> + + + + ); + } - return
{data.greeting}
; - } + renderWithClient(, { client, wrapper: Profiler }); - function App() { - return ( - - - - ); - } + { + const { renderedComponents } = await Profiler.takeRender(); - const { rerender } = render(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const skipResult = result.current; + { + const { snapshot } = await Profiler.takeRender(); - rerender(); + expect(snapshot.result).toEqual({ + data: { context: { phase: "initial" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - expect(result.current).toBe(skipResult); + await act(() => user.click(screen.getByText("Update context"))); + await Profiler.takeRender(); - // Toggle skip to `false` - await act(() => user.click(screen.getByText("Toggle skip"))); + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); - expect(screen.getByText("Loading...")).toBeInTheDocument(); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + expect(snapshot.result).toEqual({ + data: { context: { phase: "rerender" } }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } +}); - const fetchedResult = result.current; +// NOTE: We only test the `false` -> `true` path here. If the option changes +// from `true` -> `false`, the data has already been canonized, so it has no +// effect on the output. +it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { + interface Result { + __typename: string; + value: number; + } - rerender(); + interface Data { + results: Result[]; + } - expect(result.current).toBe(fetchedResult); + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, }); - it("`skip` option works with `startTransition`", async () => { - interface Data { - greeting: string; + const query: TypedDocumentNode = gql` + query { + results { + value + } } + `; - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - delay: 10, - }, - ]; + const user = userEvent.setup(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + cache.writeQuery({ + query, + data: { results }, + }); - function SuspenseFallback() { - return
Loading...
; - } + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [isPending, startTransition] = React.useTransition(); - const [queryRef] = useBackgroundQuery(query, { skip }); + function App() { + useTrackRenders(); + const [canonizeResults, setCanonizeResults] = React.useState(false); + const [queryRef] = useBackgroundQuery(query, { + canonizeResults, + }); - return ( - <> - - }> - {queryRef && } - - - ); - } + return ( + <> + + }> + + + + ); + } - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + renderWithMocks(, { cache, wrapper: Profiler }); - return
{data.greeting}
; - } + { + const { snapshot } = await Profiler.takeRender(); + const result = snapshot.result!; + const resultSet = new Set(result.data.results); + const values = Array.from(resultSet).map((item) => item.value); - function App() { - return ( - - - - ); - } + expect(result.data).toEqual({ results }); + expect(result.data.results.length).toBe(6); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); + } - render(); + await act(() => user.click(screen.getByText("Canonize results"))); - const button = screen.getByText("Toggle skip"); + { + const { snapshot } = await Profiler.takeRender(); + const result = snapshot.result!; + const resultSet = new Set(result.data.results); + const values = Array.from(resultSet).map((item) => item.value); - // Toggle skip to `false` - await act(() => user.click(button)); + expect(result.data).toEqual({ results }); + expect(result.data.results.length).toBe(6); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); + } +}); - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(button).toBeDisabled(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); +it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { + interface Data { + primes: number[]; + } - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); - }); - }); + const user = userEvent.setup(); - it("`skipToken` works with `startTransition`", async () => { - interface Data { - greeting: string; + const query: TypedDocumentNode = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) } + `; - const user = userEvent.setup(); + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + delay: 10, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + { + request: { query, variables: { min: 30, max: 50 } }, + result: { data: { primes: [31, 37, 41, 43, 47] } }, + delay: 10, + }, + ]; - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + const mergeParams: [number[] | undefined, number[]][] = []; - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - delay: 10, + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, }, - ]; + }, + }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - function SuspenseFallback() { - return
Loading...
; - } + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [isPending, startTransition] = React.useTransition(); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : undefined - ); + function App() { + useTrackRenders(); + const [refetchWritePolicy, setRefetchWritePolicy] = + React.useState("merge"); - return ( - <> - - }> - {queryRef && } - - - ); - } + const [queryRef, { refetch }] = useBackgroundQuery(query, { + refetchWritePolicy, + variables: { min: 0, max: 12 }, + }); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + return ( + <> + + + + }> + + + + ); + } - return
{data.greeting}
; - } + renderWithClient(, { client, wrapper: Profiler }); - function App() { - return ( - - - - ); - } + // initial suspended render + await Profiler.takeRender(); - render(); + { + const { snapshot } = await Profiler.takeRender(); - const button = screen.getByText("Toggle skip"); + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } - // Toggle skip to `false` - await act(() => user.click(button)); + await act(() => user.click(screen.getByText("Refetch next"))); + await Profiler.takeRender(); - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(button).toBeDisabled(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + ]); + } - it("applies `errorPolicy` on next fetch when it changes between renders", async () => { - interface Data { - greeting: string; - } + await act(() => user.click(screen.getByText("Change refetch write policy"))); + await Profiler.takeRender(); - const user = userEvent.setup(); + await act(() => user.click(screen.getByText("Refetch last"))); + await Profiler.takeRender(); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + { + const { snapshot } = await Profiler.takeRender(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, + expect(snapshot.result).toEqual({ + data: { primes: [31, 37, 41, 43, 47] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + [undefined, [31, 37, 41, 43, 47]], + ]); + } +}); + +it("applies `returnPartialData` on next fetch when it changes between renders", async () => { + const { query } = setupVariablesCase(); + + interface PartialData { + character: { + __typename: "Character"; + id: string; + }; + } + + const user = userEvent.setup(); + + const partialQuery: TypedDocumentNode = gql` + query { + character { + __typename + id + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", + }, + }, }, - { - request: { query }, - result: { - errors: [new GraphQLError("oops")], + delay: 10, + }, + { + request: { query }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange (refetched)", + }, }, }, - ]; + delay: 10, + }, + ]; - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [returnPartialData, setReturnPartialData] = React.useState(false); + const [queryRef] = useBackgroundQuery(query, { returnPartialData }); + + return ( + <> + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Doctor Strange" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
Loading...
; - } + await act(() => user.click(screen.getByText("Update partial data"))); + await Profiler.takeRender(); - function Parent() { - const [errorPolicy, setErrorPolicy] = React.useState("none"); - const [queryRef, { refetch }] = useBackgroundQuery(query, { - errorPolicy, - }); + cache.modify({ + id: cache.identify({ __typename: "Character", id: "1" }), + fields: { + name: (_, { DELETE }) => DELETE, + }, + }); - return ( - <> - - - }> - - - - ); - } + { + const { snapshot } = await Profiler.takeRender(); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data, error } = useReadQuery(queryRef); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } - return error ? -
{error.message}
- :
{data.greeting}
; - } + { + const { snapshot } = await Profiler.takeRender(); - function App() { - return ( - - Error boundary} - > - - - - ); - } + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange (refetched)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); - render(); +it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { + const { query, mocks } = setupVariablesCase(); - expect(await screen.findByTestId("greeting")).toHaveTextContent("Hello"); + const user = userEvent.setup(); + const cache = new InMemoryCache(); - await act(() => user.click(screen.getByText("Change error policy"))); - await act(() => user.click(screen.getByText("Refetch greeting"))); + cache.writeQuery({ + query, + variables: { id: "1" }, + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Cacheman", + }, + }, + }); - // Ensure we aren't rendering the error boundary and instead rendering the - // error message in the Greeting component. - expect(await screen.findByTestId("error")).toHaveTextContent("oops"); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, }); - it("applies `context` on next fetch when it changes between renders", async () => { - interface Data { - context: Record; - } + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const user = userEvent.setup(); + function App() { + const [fetchPolicy, setFetchPolicy] = + React.useState("cache-first"); - const query: TypedDocumentNode = gql` - query { - context - } - `; + const [queryRef, { refetch }] = useBackgroundQuery(query, { + fetchPolicy, + variables: { id: "1" }, + }); - const link = new ApolloLink((operation) => { - return Observable.of({ - data: { - context: operation.getContext(), + return ( + <> + + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Cacheman", }, - }); + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), + await act(() => user.click(screen.getByText("Change fetch policy"))); + { + const { snapshot } = await Profiler.takeRender(); + + // ensure we haven't changed the result yet just by changing the fetch policy + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Cacheman", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
Loading...
; - } + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); - function Parent() { - const [phase, setPhase] = React.useState("initial"); - const [queryRef, { refetch }] = useBackgroundQuery(query, { - context: { phase }, - }); + { + const { snapshot } = await Profiler.takeRender(); - return ( - <> - - + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // Because we switched to a `no-cache` fetch policy, we should not see the + // newly fetched data in the cache after the fetch occurred. + expect(cache.readQuery({ query, variables: { id: "1" } })).toEqual({ + character: { + __typename: "Character", + id: "1", + name: "Spider-Cacheman", + }, + }); +}); + +it("properly handles changing options along with changing `variables`", async () => { + const { query } = setupVariablesCase(); + const user = userEvent.setup(); + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("oops")], + }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + character: { + __typename: "Character", + id: "2", + name: "Hulk", + }, + }, + }, + delay: 10, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query, + variables: { + id: "1", + }, + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); + + function App() { + useTrackRenders(); + const [id, setId] = React.useState("1"); + + const [queryRef, { refetch }] = useBackgroundQuery(query, { + errorPolicy: id === "1" ? "all" : "none", + variables: { id }, + }); + + return ( + <> + + + + }> - + - - ); - } + + + ); + } - function Context({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + renderWithClient(, { client, wrapper: Profiler }); - return
{data.context.phase}
; - } + { + const { snapshot } = await Profiler.takeRender(); - function App() { - return ( - - - - ); - } + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } - render(); + await act(() => user.click(screen.getByText("Get second character"))); + await Profiler.takeRender(); - expect(await screen.findByTestId("context")).toHaveTextContent("initial"); + { + const { snapshot } = await Profiler.takeRender(); - await act(() => user.click(screen.getByText("Update context"))); - await act(() => user.click(screen.getByText("Refetch"))); + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "2", + name: "Hulk", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await act(() => user.click(screen.getByText("Get first character"))); + + { + const { snapshot } = await Profiler.takeRender(); - expect(await screen.findByTestId("context")).toHaveTextContent("rerender"); + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + // Ensure we render the inline error instead of the error boundary, which + // tells us the error policy was properly applied. + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), + networkStatus: NetworkStatus.error, + }, + }); + } +}); + +it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { + const { query, mocks } = setupVariablesCase(); + const cache = new InMemoryCache(); + + { + // Disable missing field warning + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + // @ts-expect-error writing partial query data + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); + } + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, }); - // NOTE: We only test the `false` -> `true` path here. If the option changes - // from `true` -> `false`, the data has already been canonized, so it has no - // effect on the output. - it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { - interface Result { - __typename: string; - value: number; + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + variables: { id: "1" }, + }); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it('suspends and does not use partial data from other variables in the cache when changing variables and using a "cache-first" fetch policy with returnPartialData: true', async () => { + const { query, mocks } = setupVariablesCase(); + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id + } } + `; - interface Data { - results: Result[]; + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App({ id }: { id: string }) { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + variables: { id }, + }); + + return ( + }> + + + ); + } + + const { rerender } = renderWithMocks(, { + cache, + mocks, + wrapper: Profiler, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + rerender(); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "2", name: "Black Widow" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { + const { query, mocks } = setupVariablesCase(); + + const partialQuery = gql` + query ($id: String!) { + character(id: $id) { + id + } } + `; - const cache = new InMemoryCache({ - typePolicies: { - Result: { - keyFields: false, - }, + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + variables: { id: "1" }, + data: { character: { __typename: "Character", id: "1" } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "network-only", + returnPartialData: true, + variables: { id: "1" }, + }); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const query: TypedDocumentNode = gql` - query { - results { - value - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { + using _consoleSpy = spyOnConsole("warn"); + const { query, mocks } = setupVariablesCase(); + + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id } - `; + } + `; - const results: Result[] = [ - { __typename: "Result", value: 0 }, - { __typename: "Result", value: 1 }, - { __typename: "Result", value: 1 }, - { __typename: "Result", value: 2 }, - { __typename: "Result", value: 3 }, - { __typename: "Result", value: 5 }, - ]; + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - const user = userEvent.setup(); + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - cache.writeQuery({ - query, - data: { results }, + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + variables: { id: "1" }, }); - const client = new ApolloClient({ - link: new MockLink([]), - cache, - }); + return ( + }> + + + ); + } - const result: { current: Data | null } = { - current: null, - }; + renderWithClient(, { client, wrapper: Profiler }); - function SuspenseFallback() { - return
Loading...
; - } + { + const { renderedComponents } = await Profiler.takeRender(); - function Parent() { - const [canonizeResults, setCanonizeResults] = React.useState(false); - const [queryRef] = useBackgroundQuery(query, { - canonizeResults, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - return ( - <> - - }> - - - - ); - } + { + const { snapshot } = await Profiler.takeRender(); - function Results({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - result.current = data; + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - return null; - } +it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { + using _consoleSpy = spyOnConsole("warn"); - function App() { - return ( - - - - ); + const query: TypedDocumentNode = gql` + query UserQuery { + greeting } + `; + const mocks = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + }, + ]; - render(); + renderHook( + () => + useBackgroundQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - function verifyCanonicalResults(data: Data, canonized: boolean) { - const resultSet = new Set(data.results); - const values = Array.from(resultSet).map((item) => item.value); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy." + ); +}); - expect(data).toEqual({ results }); +it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { + const { query, mocks } = setupVariablesCase(); - if (canonized) { - expect(data.results.length).toBe(6); - expect(resultSet.size).toBe(5); - expect(values).toEqual([0, 1, 2, 3, 5]); - } else { - expect(data.results.length).toBe(6); - expect(resultSet.size).toBe(6); - expect(values).toEqual([0, 1, 1, 2, 3, 5]); + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id } } + `; - verifyCanonicalResults(result.current!, false); + const cache = new InMemoryCache(); - await act(() => user.click(screen.getByText("Canonize results"))); + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); - verifyCanonicalResults(result.current!, true); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, }); - it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { - interface Data { - primes: number[]; - } + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const user = userEvent.setup(); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-and-network", + returnPartialData: true, + variables: { id: "1" }, + }); - const query: TypedDocumentNode = gql` - query GetPrimes($min: number, $max: number) { - primes(min: $min, max: $max) - } - `; + return ( + }> + + + ); + } - const mocks = [ - { - request: { query, variables: { min: 0, max: 12 } }, - result: { data: { primes: [2, 3, 5, 7, 11] } }, - }, - { - request: { query, variables: { min: 12, max: 30 } }, - result: { data: { primes: [13, 17, 19, 23, 29] } }, - delay: 10, - }, - { - request: { query, variables: { min: 30, max: 50 } }, - result: { data: { primes: [31, 37, 41, 43, 47] } }, - delay: 10, - }, - ]; + renderWithClient(, { client, wrapper: Profiler }); - const mergeParams: [number[] | undefined, number[]][] = []; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - primes: { - keyArgs: false, - merge(existing: number[] | undefined, incoming: number[]) { - mergeParams.push([existing, incoming]); - return existing ? existing.concat(incoming) : incoming; - }, - }, - }, - }, - }, + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, }); + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
Loading...
; - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - function Parent() { - const [refetchWritePolicy, setRefetchWritePolicy] = - React.useState("merge"); +it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { + const { query, mocks } = setupVariablesCase(); + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id + } + } + `; - const [queryRef, { refetch }] = useBackgroundQuery(query, { - refetchWritePolicy, - variables: { min: 0, max: 12 }, - }); + const cache = new InMemoryCache(); - return ( - <> - - - - }> - - - - ); - } + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); - function Primes({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - return {data.primes.join(", ")}; - } + function App({ id }: { id: string }) { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-and-network", + returnPartialData: true, + variables: { id }, + }); - function App() { - return ( - - - - ); - } + return ( + }> + + + ); + } - render(); + const { rerender } = renderWithMocks(, { + cache, + mocks, + wrapper: Profiler, + }); - const primes = await screen.findByTestId("primes"); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(primes).toHaveTextContent("2, 3, 5, 7, 11"); - expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } - await act(() => user.click(screen.getByText("Refetch next"))); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - await waitFor(() => { - expect(primes).toHaveTextContent("2, 3, 5, 7, 11, 13, 17, 19, 23, 29"); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - expect(mergeParams).toEqual([ - [undefined, [2, 3, 5, 7, 11]], - [ - [2, 3, 5, 7, 11], - [13, 17, 19, 23, 29], - ], - ]); + rerender(); - await act(() => - user.click(screen.getByText("Change refetch write policy")) - ); + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - await act(() => user.click(screen.getByText("Refetch last"))); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - await waitFor(() => { - expect(primes).toHaveTextContent("31, 37, 41, 43, 47"); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "2", name: "Black Widow" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - expect(mergeParams).toEqual([ - [undefined, [2, 3, 5, 7, 11]], - [ - [2, 3, 5, 7, 11], - [13, 17, 19, 23, 29], - ], - [undefined, [31, 37, 41, 43, 47]], - ]); - }); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - it("applies `returnPartialData` on next fetch when it changes between renders", async () => { - interface Data { - character: { - __typename: "Character"; - id: string; +it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + interface QueryData { + greeting: { + __typename: string; + message?: string; + recipient?: { + __typename: string; name: string; }; - } + }; + } - interface PartialData { - character: { - __typename: "Character"; - id: string; - }; + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } } + `; - const user = userEvent.setup(); + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); - const fullQuery: TypedDocumentNode = gql` - query { - character { - __typename - id - name - } - } - `; + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } - const partialQuery: TypedDocumentNode = gql` - query { - character { - __typename - id - } - } - `; + const client = new ApolloClient({ link, cache }); - const mocks = [ - { - request: { query: fullQuery }, - result: { - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strange", + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + link.simulateResult({ + result: { + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + hasNext: true, + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, }, + path: ["greeting"], }, + ], + hasNext: false, + }, + }, + true + ); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, }, }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +describe("refetch", () => { + it("re-suspends when calling `refetch`", async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + const mocks: MockedResponse[] = [ + ...defaultMocks, { - request: { query: fullQuery }, + request: { query, variables: { id: "1" } }, result: { data: { character: { __typename: "Character", id: "1", - name: "Doctor Strange (refetched)", + name: "Spider-Man (refetched)", }, }, }, - delay: 100, + delay: 10, }, ]; - const cache = new InMemoryCache(); - - cache.writeQuery({ - query: partialQuery, - data: { character: { __typename: "Character", id: "1" } }, - }); - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function SuspenseFallback() { - return
Loading...
; - } - - function Parent() { - const [returnPartialData, setReturnPartialData] = React.useState(false); - - const [queryRef] = useBackgroundQuery(fullQuery, { - returnPartialData, + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, }); return ( <> - + }> - + ); } - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + renderWithMocks(, { mocks, wrapper: Profiler }); - return ( - {data.character.name ?? "unknown"} - ); - } + { + const { renderedComponents } = await Profiler.takeRender(); - function App() { - return ( - - - - ); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } - render(); - - const character = await screen.findByTestId("character"); - - expect(character).toHaveTextContent("Doctor Strange"); - - await act(() => user.click(screen.getByText("Update partial data"))); + { + const { snapshot } = await Profiler.takeRender(); - cache.modify({ - id: cache.identify({ __typename: "Character", id: "1" }), - fields: { - name: (_, { DELETE }) => DELETE, - }, - }); + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - await waitFor(() => { - expect(character).toHaveTextContent("unknown"); - }); + await act(() => user.click(screen.getByText("Refetch"))); - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strange (refetched)"); - }); - }); + { + // parent component re-suspends + const { renderedComponents } = await Profiler.takeRender(); - it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { - interface Data { - character: { - __typename: "Character"; - id: string; - name: string; - }; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query { - character { - __typename - id - name - } - } - `; + { + const { snapshot } = await Profiler.takeRender(); - const mocks = [ - { - request: { query }, - result: { - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strange", - }, + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched)", }, }, - delay: 10, - }, - ]; - - const cache = new InMemoryCache(); - - cache.writeQuery({ - query, - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strangecache", - }, - }, - }); - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function SuspenseFallback() { - return
Loading...
; + error: undefined, + networkStatus: NetworkStatus.ready, + }); } - function Parent() { - const [fetchPolicy, setFetchPolicy] = - React.useState("cache-first"); + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); + + it("re-suspends when calling `refetch` with new variables", async () => { + const { query, mocks } = setupVariablesCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + function App() { + useTrackRenders(); const [queryRef, { refetch }] = useBackgroundQuery(query, { - fetchPolicy, + variables: { id: "1" }, }); return ( <> - - + }> - + ); } - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + renderWithMocks(, { mocks, wrapper: Profiler }); - return {data.character.name}; - } + { + const { renderedComponents } = await Profiler.takeRender(); - function App() { - return ( - - - - ); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } - render(); - - const character = await screen.findByTestId("character"); + { + const { snapshot } = await Profiler.takeRender(); - expect(character).toHaveTextContent("Doctor Strangecache"); + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - await act(() => user.click(screen.getByText("Change fetch policy"))); await act(() => user.click(screen.getByText("Refetch"))); - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strange"); - }); - // Because we switched to a `no-cache` fetch policy, we should not see the - // newly fetched data in the cache after the fetch occurred. - expect(cache.readQuery({ query })).toEqual({ - character: { - __typename: "Character", - id: "1", - name: "Doctor Strangecache", - }, - }); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - it("properly handles changing options along with changing `variables`", async () => { - interface Data { - character: { - __typename: "Character"; - id: string; - name: string; - }; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "2", + name: "Black Widow", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); } + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); + + it("re-suspends multiple times when calling `refetch` multiple times", async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); const user = userEvent.setup(); - const query: TypedDocumentNode = gql` - query ($id: ID!) { - character(id: $id) { - __typename - id - name - } - } - `; + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const mocks = [ + const mocks: MockedResponse[] = [ + ...defaultMocks, { request: { query, variables: { id: "1" } }, result: { - errors: [new GraphQLError("oops")], + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched)", + }, + }, }, delay: 10, }, { - request: { query, variables: { id: "2" } }, + request: { query, variables: { id: "1" } }, result: { data: { character: { __typename: "Character", - id: "2", - name: "Hulk", + id: "1", + name: "Spider-Man (refetched again)", }, }, }, @@ -3071,1309 +3420,965 @@ describe("useBackgroundQuery", () => { }, ]; - const cache = new InMemoryCache(); - - cache.writeQuery({ - query, - variables: { - id: "1", - }, - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strangecache", - }, - }, - }); - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function SuspenseFallback() { - return
Loading...
; - } - - function Parent() { - const [id, setId] = React.useState("1"); - + function App() { + useTrackRenders(); const [queryRef, { refetch }] = useBackgroundQuery(query, { - errorPolicy: id === "1" ? "all" : "none", - variables: { id }, + variables: { id: "1" }, }); return ( <> - - - Error boundary} - > - }> - - - + }> + + ); } - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data, error } = useReadQuery(queryRef); + renderWithMocks(, { mocks, wrapper: Profiler }); - return error ? -
{error.message}
- : {data.character.name}; - } + { + const { renderedComponents } = await Profiler.takeRender(); - function App() { - return ( - - - - ); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } - render(); - - const character = await screen.findByTestId("character"); - - expect(character).toHaveTextContent("Doctor Strangecache"); + { + const { snapshot } = await Profiler.takeRender(); - await act(() => user.click(screen.getByText("Get second character"))); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - await waitFor(() => { - expect(character).toHaveTextContent("Hulk"); - }); + const button = screen.getByText("Refetch"); - await act(() => user.click(screen.getByText("Get first character"))); + await act(() => user.click(button)); - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strangecache"); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - await act(() => user.click(screen.getByText("Refetch"))); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - // Ensure we render the inline error instead of the error boundary, which - // tells us the error policy was properly applied. - expect(await screen.findByTestId("error")).toHaveTextContent("oops"); - }); + { + const { snapshot } = await Profiler.takeRender(); - describe("refetch", () => { - it("re-suspends when calling `refetch`", async () => { - const { ProfiledApp } = renderVariablesIntegrationTest({ - variables: { id: "1" }, + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - { - const { withinDOM, snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(1); - expect(withinDOM().getByText("loading")).toBeInTheDocument(); - } - - { - const { withinDOM } = await ProfiledApp.takeRender(); - expect(withinDOM().getByText("1 - Spider-Man")).toBeInTheDocument(); - } - - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + await act(() => user.click(button)); - { - // parent component re-suspends - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(2); - } - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - // @jerelmiller can you please verify that this is still in the spirit of the test? - // This seems to have moved onto the next render - or before the test skipped one. - expect(snapshot.count).toBe(2); - expect( - withinDOM().getByText("1 - Spider-Man (updated)") - ).toBeInTheDocument(); - } + { + const { renderedComponents } = await Profiler.takeRender(); - expect(ProfiledApp).not.toRerender(); - }); - it("re-suspends when calling `refetch` with new variables", async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; + { + const { snapshot } = await Profiler.takeRender(); - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched again)", }, - delay: 200, }, - { - request: { query, variables: { id: "2" } }, - result: { - data: { character: { id: "2", name: "Captain America" } }, - }, - delay: 200, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); + + it("throws errors when errors are returned after calling `refetch`", async () => { + using _consoleSpy = spyOnConsole("error"); + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const mocks: MockedResponse[] = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], }, - ]; + delay: 10, + }, + ]; + + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); - const { renders } = renderVariablesIntegrationTest({ + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { variables: { id: "1" }, - mocks, }); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + return ( + <> + + }> + + + + + + ); + } - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + renderWithMocks(, { mocks, wrapper: Profiler }); - const newVariablesRefetchButton = screen.getByText( - "Set variables to id: 2" - ); - const refetchButton = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(newVariablesRefetchButton)); - await act(() => user.click(refetchButton)); + { + const { renderedComponents } = await Profiler.takeRender(); - expect( - await screen.findByText("2 - Captain America") - ).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(3); + { + const { snapshot } = await Profiler.takeRender(); - // extra render puts an additional frame into the array - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, error: undefined, - }, - { - ...mocks[1].result, networkStatus: NetworkStatus.ready, - error: undefined, }, - ]); - }); - it("re-suspends multiple times when calling `refetch` multiple times", async () => { - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, }); + } - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + await act(() => user.click(screen.getByText("Refetch"))); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + { + const { renderedComponents } = await Profiler.takeRender(); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(1); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect( - await screen.findByText("1 - Spider-Man (updated)") - ).toBeInTheDocument(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - await act(() => user.click(button)); + expect(renderedComponents).toStrictEqual(["ErrorFallback"]); + expect(snapshot.error).toEqual( + new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }) + ); + } - // parent component re-suspends - expect(renders.suspenseCount).toBe(3); - expect(renders.count).toBe(2); + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - expect( - await screen.findByText("1 - Spider-Man (updated again)") - ).toBeInTheDocument(); + it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const mocks = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + delay: 10, + }, + ]; - expect(renders.count).toBe(3); - }); - it("throws errors when errors are returned after calling `refetch`", async () => { - using _consoleSpy = spyOnConsole("error"); - interface QueryData { - character: { - id: string; - name: string; - }; - } + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], - }, - }, - ]; - const { renders } = renderVariablesIntegrationTest({ + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { variables: { id: "1" }, - mocks, + errorPolicy: "ignore", }); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + return ( + <> + + }> + + + + + + ); + } - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + renderWithMocks(, { mocks, wrapper: Profiler }); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + { + const { renderedComponents } = await Profiler.takeRender(); - await waitFor(() => { - expect(renders.errorCount).toBe(1); - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(renders.errors).toEqual([ - new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }), - ]); - }); - it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + { + const { snapshot } = await Profiler.takeRender(); - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, }, + error: undefined, + networkStatus: NetworkStatus.ready, }, - ]; - - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "ignore", - mocks, }); + } - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + await act(() => user.click(screen.getByText("Refetch"))); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); - }); - it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, }, + error: undefined, + networkStatus: NetworkStatus.ready, }, - ]; - - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "all", - mocks, }); + } - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); - - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); - - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); - - expect( - await screen.findByText("Something went wrong") - ).toBeInTheDocument(); - }); - it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: null } }, - errors: [new GraphQLError("Something went wrong")], - }, + it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const mocks = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], }, - ]; + delay: 10, + }, + ]; - const { renders } = renderVariablesIntegrationTest({ + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { variables: { id: "1" }, errorPolicy: "all", - mocks, }); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + return ( + <> + + }> + + + + + + ); + } - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + renderWithMocks(, { mocks, wrapper: Profiler }); - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); + { + const { renderedComponents } = await Profiler.takeRender(); - expect( - await screen.findByText("Something went wrong") - ).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const expectedError = new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }); + { + const { snapshot } = await Profiler.takeRender(); - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, error: undefined, + networkStatus: NetworkStatus.ready, }, - { - data: mocks[1].result.data, - networkStatus: NetworkStatus.error, - error: expectedError, - }, - ]); - }); + }); + } - it("can refetch after error is encountered", async () => { - type Variables = { - id: string; - }; + await act(() => user.click(screen.getByText("Refetch"))); - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); + { + const { renderedComponents } = await Profiler.takeRender(); - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } - } - `; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: null, - errors: [new GraphQLError("Oops couldn't fetch")], - }, - delay: 10, - }, - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: true } }, + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, }, - delay: 10, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), }); + } - function App() { - return ( - - - - ); - } + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - function SuspenseFallback() { - return

Loading

; - } + it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const mocks = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { __typename: "Character", id: "1", name: null } }, + errors: [new GraphQLError("Something went wrong")], + }, + delay: 10, + }, + ]; + + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); - function Parent() { - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { id: "1" }, - }); + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + errorPolicy: "all", + }); - return ( + return ( + <> + }> - refetch()} - fallbackRender={({ error, resetErrorBoundary }) => ( - <> - -
{error.message}
- - )} - > - + +
- ); - } + + ); + } - function Todo({ queryRef }: { queryRef: QueryReference }) { - const { - data: { todo }, - } = useReadQuery(queryRef); - - return ( -
- {todo.name} - {todo.completed && " (completed)"} -
- ); - } + renderWithMocks(, { mocks, wrapper: Profiler }); - render(); + { + const { renderedComponents } = await Profiler.takeRender(); - // Disable error message shown in the console due to an uncaught error. - // TODO: need to determine why the error message is logged to the console - // as an uncaught error since other tests do not require this. - { - using _consoleSpy = spyOnConsole("error"); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(screen.getByText("Loading")).toBeInTheDocument(); + { + const { snapshot } = await Profiler.takeRender(); - expect( - await screen.findByText("Oops couldn't fetch") - ).toBeInTheDocument(); - } + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); - const button = screen.getByText("Retry"); + { + const { renderedComponents } = await Profiler.takeRender(); - await act(() => user.click(button)); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(screen.getByText("Loading")).toBeInTheDocument(); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("todo")).toHaveTextContent( - "Clean room (completed)" - ); + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: null, + }, + }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }, }); - }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); + + it("can refetch after error is encountered", async () => { + type Variables = { + id: string; + }; - it("throws errors on refetch after error is encountered after first fetch with error", async () => { - type Variables = { + interface Data { + todo: { id: string; + name: string; + completed: boolean; }; + } + const user = userEvent.setup(); - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed } - `; + } + `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: null, - errors: [new GraphQLError("Oops couldn't fetch")], - }, - delay: 10, + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: null, + errors: [new GraphQLError("Oops couldn't fetch")], }, - { - request: { query, variables: { id: "1" } }, - result: { - data: null, - errors: [new GraphQLError("Oops couldn't fetch again")], - }, - delay: 10, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, }, - ]; + delay: 10, + }, + ]; + + const Profiler = createErrorProfiler(); + const { SuspenseFallback } = createDefaultTrackedComponents(Profiler); + + function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + useTrackRenders(); + Profiler.mergeSnapshot({ error }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + return ; + } + + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, }); - function App() { - return ( - - - - ); - } + return ( + }> + refetch()} + FallbackComponent={ErrorFallback} + > + + + + ); + } - function SuspenseFallback() { - return

Loading

; - } + function Todo({ queryRef }: { queryRef: QueryReference }) { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); - function Parent() { - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { id: "1" }, - }); + return null; + } - return ( - }> - refetch()} - fallbackRender={({ error, resetErrorBoundary }) => ( - <> - -
{error.message}
- - )} - > - -
-
- ); - } + renderWithMocks(, { mocks, wrapper: Profiler }); - function Todo({ queryRef }: { queryRef: QueryReference }) { - const { - data: { todo }, - } = useReadQuery(queryRef); - - return ( -
- {todo.name} - {todo.completed && " (completed)"} -
- ); - } + { + const { renderedComponents } = await Profiler.takeRender(); - render(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + { // Disable error message shown in the console due to an uncaught error. - // TODO: need to determine why the error message is logged to the console - // as an uncaught error since other tests do not require this. using _consoleSpy = spyOnConsole("error"); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(screen.getByText("Loading")).toBeInTheDocument(); - - expect( - await screen.findByText("Oops couldn't fetch") - ).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(snapshot).toEqual({ + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Oops couldn't fetch")], + }), + result: null, + }); + } - const button = screen.getByText("Retry"); + await act(() => user.click(screen.getByText("Retry"))); - await act(() => user.click(button)); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - await waitFor(() => { - expect( - screen.getByText("Oops couldn't fetch again") - ).toBeInTheDocument(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([Todo]); + expect(snapshot).toEqual({ + // TODO: We should reset the snapshot between renders to better capture + // the actual result. This makes it seem like the error is rendered, but + // in this is just leftover from the previous snapshot. + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Oops couldn't fetch")], + }), + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, }); + } + }); - expect(screen.queryByText("Loading")).not.toBeInTheDocument(); - }); + it("throws errors on refetch after error is encountered after first fetch with error", async () => { + // Disable error message shown in the console due to an uncaught error. + using _consoleSpy = spyOnConsole("error"); + type Variables = { + id: string; + }; - it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { - type Variables = { + interface Data { + todo: { id: string; + name: string; + completed: boolean; }; + } - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } + const user = userEvent.setup(); + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed } - `; - - const mocks: MockedResponse[] = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: false } }, - }, - delay: 10, - }, - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: true } }, - }, - delay: 10, - }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); - - function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - return

Loading

; - } - - function Parent() { - const [id, setId] = React.useState("1"); - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { id }, - }); - const onRefetchHandler = () => { - refetch(); - }; - return ( - - ); - } - - function Todo({ - queryRef, - onRefetch, - }: { - onRefetch: () => void; - queryRef: QueryReference; - onChange: (id: string) => void; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todo } = data; - - return ( - <> - -
- {todo.name} - {todo.completed && " (completed)"} -
- - ); } + `; - const ProfiledApp = profile({ Component: App, snapshotDOM: true }); - - render(); - + const mocks = [ { - const { withinDOM } = await ProfiledApp.takeRender(); - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - } - + request: { query, variables: { id: "1" } }, + result: { + data: null, + errors: [new GraphQLError("Oops couldn't fetch")], + }, + delay: 10, + }, { - const { withinDOM } = await ProfiledApp.takeRender(); - const todo = withinDOM().getByTestId("todo"); - expect(todo).toBeInTheDocument(); - expect(todo).toHaveTextContent("Clean room"); - } - - const button = screen.getByText("Refresh"); - await act(() => user.click(button)); + request: { query, variables: { id: "1" } }, + result: { + data: null, + errors: [new GraphQLError("Oops couldn't fetch again")], + }, + delay: 10, + }, + ]; - // startTransition will avoid rendering the suspense fallback for already - // revealed content if the state update inside the transition causes the - // component to suspend. - // - // Here we should not see the suspense fallback while the component suspends - // until the todo is finished loading. Seeing the suspense fallback is an - // indication that we are suspending the component too late in the process. - { - const { withinDOM } = await ProfiledApp.takeRender(); - const todo = withinDOM().getByTestId("todo"); - expect(withinDOM().queryByText("Loading")).not.toBeInTheDocument(); + const Profiler = createErrorProfiler(); + const { SuspenseFallback } = createDefaultTrackedComponents(Profiler); - // We can ensure this works with isPending from useTransition in the process - expect(todo).toHaveAttribute("aria-busy", "true"); + function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + useTrackRenders(); + Profiler.mergeSnapshot({ error }); - // Ensure we are showing the stale UI until the new todo has loaded - expect(todo).toHaveTextContent("Clean room"); - } + return ; + } - // Eventually we should see the updated todo content once its done - // suspending. - { - const { withinDOM } = await ProfiledApp.takeRender(); - const todo = withinDOM().getByTestId("todo"); - expect(todo).toHaveTextContent("Clean room (completed)"); - } - }); - }); + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + }); - describe("fetchMore", () => { - function getItemTexts( - screen: Pick = _screen - ) { - return screen.getAllByTestId(/letter/).map( - // eslint-disable-next-line testing-library/no-node-access - (li) => li.firstChild!.textContent + return ( + }> + refetch()} + FallbackComponent={ErrorFallback} + > + + + ); } - it("re-suspends when calling `fetchMore` with different variables", async () => { - const { ProfiledApp } = renderPaginatedIntegrationTest(); + function Todo({ queryRef }: { queryRef: QueryReference }) { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); - { - const { withinDOM, snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(1); - expect(withinDOM().getByText("loading")).toBeInTheDocument(); - } + return null; + } - { - const { withinDOM } = await ProfiledApp.takeRender(); - const items = await screen.findAllByTestId(/letter/i); - expect(items).toHaveLength(2); - expect(getItemTexts(withinDOM())).toStrictEqual(["A", "B"]); - } + renderWithMocks(, { mocks, wrapper: Profiler }); - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + { + const { renderedComponents } = await Profiler.takeRender(); - { - // parent component re-suspends - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(2); - } - { - // parent component re-suspends - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - expect(snapshot.count).toBe(2); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(getItemTexts(withinDOM())).toStrictEqual(["C", "D"]); - } - }); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - it("properly uses `updateQuery` when calling `fetchMore`", async () => { - const { ProfiledApp } = renderPaginatedIntegrationTest({ - updateQuery: true, + expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(snapshot).toEqual({ + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Oops couldn't fetch")], + }), + result: null, }); + } - { - const { withinDOM, snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(1); - expect(withinDOM().getByText("loading")).toBeInTheDocument(); - } - - { - const { withinDOM } = await ProfiledApp.takeRender(); - - const items = withinDOM().getAllByTestId(/letter/i); + await act(() => user.click(screen.getByText("Retry"))); - expect(items).toHaveLength(2); - expect(getItemTexts(withinDOM())).toStrictEqual(["A", "B"]); - } + { + const { renderedComponents } = await Profiler.takeRender(); - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - { - const { snapshot } = await ProfiledApp.takeRender(); - // parent component re-suspends - expect(snapshot.suspenseCount).toBe(2); - } - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - expect(snapshot.count).toBe(2); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const moreItems = withinDOM().getAllByTestId(/letter/i); - expect(moreItems).toHaveLength(4); - expect(getItemTexts(withinDOM())).toStrictEqual(["A", "B", "C", "D"]); - } - }); - it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { - const { ProfiledApp } = renderPaginatedIntegrationTest({ - fieldPolicies: true, + expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(snapshot).toEqual({ + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Oops couldn't fetch again")], + }), + result: null, }); + } + }); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(1); - expect(withinDOM().getByText("loading")).toBeInTheDocument(); - } - - { - const { withinDOM } = await ProfiledApp.takeRender(); - const items = withinDOM().getAllByTestId(/letter/i); - expect(items).toHaveLength(2); - expect(getItemTexts(withinDOM())).toStrictEqual(["A", "B"]); - } + it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + id: string; + }; - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + interface Data { + todo: { + id: string; + name: string; + completed: boolean; + }; + } + const user = userEvent.setup(); - { - const { snapshot } = await ProfiledApp.takeRender(); - // parent component re-suspends - expect(snapshot.suspenseCount).toBe(2); + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed + } } + `; + const mocks: MockedResponse[] = [ { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - expect(snapshot.count).toBe(2); - const moreItems = await screen.findAllByTestId(/letter/i); - expect(moreItems).toHaveLength(4); - expect(getItemTexts(withinDOM())).toStrictEqual(["A", "B", "C", "D"]); - } - }); - it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { - type Variables = { - offset: number; - }; + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + }, + delay: 10, + }, + ]; - interface Todo { - __typename: "Todo"; - id: string; - name: string; - completed: boolean; - } - interface Data { - todos: Todo[]; - } - const user = userEvent.setup(); + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); - const query: TypedDocumentNode = gql` - query TodosQuery($offset: Int!) { - todos(offset: $offset) { - id - name - completed - } - } - `; + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const mocks: MockedResponse[] = [ - { - request: { query, variables: { offset: 0 } }, - result: { - data: { - todos: [ - { - __typename: "Todo", - id: "1", - name: "Clean room", - completed: false, - }, - ], - }, - }, - delay: 10, - }, - { - request: { query, variables: { offset: 1 } }, - result: { - data: { - todos: [ - { - __typename: "Todo", - id: "2", - name: "Take out trash", - completed: true, - }, - ], - }, - }, - delay: 10, - }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - todos: offsetLimitPagination(), - }, - }, - }, - }), + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, }); - function App() { - return ( - - }> - - - - ); - } + Profiler.mergeSnapshot({ isPending }); - function SuspenseFallback() { - return

Loading

; - } + return ( + <> + + }> + + + + ); + } - function Parent() { - const [queryRef, { fetchMore }] = useBackgroundQuery(query, { - variables: { offset: 0 }, - }); - const onFetchMoreHandler = (variables: Variables) => { - fetchMore({ variables }); - }; - return ; - } + renderWithMocks(, { mocks, wrapper: Profiler }); - function Todo({ - queryRef, - onFetchMore, - }: { - onFetchMore: (variables: Variables) => void; - queryRef: QueryReference; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todos } = data; - - return ( - <> - -
- {todos.map((todo) => ( -
- {todo.name} - {todo.completed && " (completed)"} -
- ))} -
- - ); - } + { + const { renderedComponents } = await Profiler.takeRender(); - const ProfiledApp = profile({ Component: App, snapshotDOM: true }); - render(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - { - const { withinDOM } = await ProfiledApp.takeRender(); - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - } + { + const { snapshot } = await Profiler.takeRender(); - { - const { withinDOM } = await ProfiledApp.takeRender(); - expect(withinDOM().getByTestId("todos")).toBeInTheDocument(); - expect(withinDOM().getByTestId("todo:1")).toBeInTheDocument(); - } + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } - const button = screen.getByText("Load more"); - await act(() => user.click(button)); + await act(() => user.click(screen.getByText("Refetch"))); - { - const { withinDOM } = await ProfiledApp.takeRender(); - // startTransition will avoid rendering the suspense fallback for already - // revealed content if the state update inside the transition causes the - // component to suspend. - // - // Here we should not see the suspense fallback while the component suspends - // until the todo is finished loading. Seeing the suspense fallback is an - // indication that we are suspending the component too late in the process. - expect(withinDOM().queryByText("Loading")).not.toBeInTheDocument(); - - // We can ensure this works with isPending from useTransition in the process - expect(withinDOM().getByTestId("todos")).toHaveAttribute( - "aria-busy", - "true" - ); - - // Ensure we are showing the stale UI until the new todo has loaded - expect(withinDOM().getByTestId("todo:1")).toHaveTextContent( - "Clean room" - ); - } + { + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component + // suspends until the todo is finished loading. Seeing the suspense + // fallback is an indication that we are suspending the component too late + // in the process. + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: true, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } - { - const { withinDOM } = await ProfiledApp.takeRender(); - // Eventually we should see the updated todos content once its done - // suspending. - expect(withinDOM().getByTestId("todo:2")).toHaveTextContent( - "Take out trash (completed)" - ); - expect(withinDOM().getByTestId("todo:1")).toHaveTextContent( - "Clean room" - ); - } - }); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + // Eventually we should see the updated todo content once its done + // suspending. + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + }); - it('honors refetchWritePolicy set to "merge"', async () => { - const user = userEvent.setup(); + it('honors refetchWritePolicy set to "merge"', async () => { + const user = userEvent.setup(); - const query: TypedDocumentNode< - { primes: number[] }, - { min: number; max: number } - > = gql` + const query: TypedDocumentNode = + gql` query GetPrimes($min: number, $max: number) { primes(min: $min, max: $max) } `; - interface QueryData { - primes: number[]; - } + interface QueryData { + primes: number[]; + } - const mocks = [ - { - request: { query, variables: { min: 0, max: 12 } }, - result: { data: { primes: [2, 3, 5, 7, 11] } }, - }, - { - request: { query, variables: { min: 12, max: 30 } }, - result: { data: { primes: [13, 17, 19, 23, 29] } }, - delay: 10, - }, - ]; + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + delay: 10, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + ]; - const mergeParams: [number[] | undefined, number[]][] = []; - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - primes: { - keyArgs: false, - merge(existing: number[] | undefined, incoming: number[]) { - mergeParams.push([existing, incoming]); - return existing ? existing.concat(incoming) : incoming; - }, + const mergeParams: [number[] | undefined, number[]][] = []; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; }, }, }, }, - }); + }, + }); - function SuspenseFallback() { - return
loading
; - } + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { min: 0, max: 12 }, + refetchWritePolicy: "merge", }); - function Child({ - refetch, - queryRef, - }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); - - return ( -
- -
{data?.primes.join(", ")}
-
{networkStatus}
-
{error?.message || "undefined"}
-
- ); - } + return ( + <> + + }> + + + + ); + } - function Parent() { - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { min: 0, max: 12 }, - refetchWritePolicy: "merge", - }); - return ; - } + renderWithClient(, { client, wrapper: Profiler }); - function App() { - return ( - - }> - - - - ); - } + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - render(); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("primes")).toHaveTextContent( - "2, 3, 5, 7, 11" - ); + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - expect(screen.getByTestId("network-status")).toHaveTextContent( - "7" // ready - ); - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - await act(() => user.click(screen.getByText("Refetch"))); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("primes")).toHaveTextContent( - "2, 3, 5, 7, 11, 13, 17, 19, 23, 29" - ); + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - expect(screen.getByTestId("network-status")).toHaveTextContent( - "7" // ready - ); - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); expect(mergeParams).toEqual([ [undefined, [2, 3, 5, 7, 11]], [ @@ -4381,1406 +4386,1052 @@ describe("useBackgroundQuery", () => { [13, 17, 19, 23, 29], ], ]); - }); + } + }); - it('defaults refetchWritePolicy to "overwrite"', async () => { - const user = userEvent.setup(); + it('defaults refetchWritePolicy to "overwrite"', async () => { + const user = userEvent.setup(); - const query: TypedDocumentNode< - { primes: number[] }, - { min: number; max: number } - > = gql` + const query: TypedDocumentNode = + gql` query GetPrimes($min: number, $max: number) { primes(min: $min, max: $max) } `; - interface QueryData { - primes: number[]; - } + interface QueryData { + primes: number[]; + } - const mocks = [ - { - request: { query, variables: { min: 0, max: 12 } }, - result: { data: { primes: [2, 3, 5, 7, 11] } }, - }, - { - request: { query, variables: { min: 12, max: 30 } }, - result: { data: { primes: [13, 17, 19, 23, 29] } }, - delay: 10, - }, - ]; + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + delay: 10, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + ]; - const mergeParams: [number[] | undefined, number[]][] = []; - const cache = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - primes: { - keyArgs: false, - merge(existing: number[] | undefined, incoming: number[]) { - mergeParams.push([existing, incoming]); - return existing ? existing.concat(incoming) : incoming; - }, + const mergeParams: [number[] | undefined, number[]][] = []; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; }, }, }, }, - }); - - function SuspenseFallback() { - return
loading
; - } - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function Child({ - refetch, - queryRef, - }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); - - return ( -
- -
{data?.primes.join(", ")}
-
{networkStatus}
-
{error?.message || "undefined"}
-
- ); - } - - function Parent() { - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { min: 0, max: 12 }, - }); - return ; - } - - function App() { - return ( - - }> - - - - ); - } - - render(); - - await waitFor(() => { - expect(screen.getByTestId("primes")).toHaveTextContent( - "2, 3, 5, 7, 11" - ); - }); - expect(screen.getByTestId("network-status")).toHaveTextContent( - "7" // ready - ); - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); - - await act(() => user.click(screen.getByText("Refetch"))); - - await waitFor(() => { - expect(screen.getByTestId("primes")).toHaveTextContent( - "13, 17, 19, 23, 29" - ); - }); - expect(screen.getByTestId("network-status")).toHaveTextContent( - "7" // ready - ); - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - expect(mergeParams).toEqual([ - [undefined, [2, 3, 5, 7, 11]], - [undefined, [13, 17, 19, 23, 29]], - ]); + }, }); - it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { - interface Data { - character: { - id: string; - name: string; - }; - } - - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; - - const partialQuery = gql` - query { - character { - id - } - } - `; - const mocks = [ - { - request: { query: fullQuery }, - result: { data: { character: { id: "1", name: "Doctor Strange" } } }, - }, - ]; - - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - }; - - const cache = new InMemoryCache(); - - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } - - function Parent() { - const [queryRef] = useBackgroundQuery(fullQuery, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); - return ; - } - - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.count++; - - return ( - <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } - - render(); - - expect(renders.suspenseCount).toBe(0); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - expect(screen.getByTestId("character-name")).toHaveTextContent(""); - expect(screen.getByTestId("network-status")).toHaveTextContent("1"); // loading - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - - await waitFor(() => { - expect(screen.getByTestId("character-name")).toHaveTextContent( - "Doctor Strange" - ); - }); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(0); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, }); - it('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => { - const partialQuery = gql` - query ($id: ID!) { - character(id: $id) { - id - } - } - `; - - const cache = new InMemoryCache(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - variables: { id: "1" }, + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { min: 0, max: 12 }, }); - const { renders, mocks, rerender } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - cache, - options: { - fetchPolicy: "cache-first", - returnPartialData: true, - }, - }); - expect(renders.suspenseCount).toBe(0); + return ( + <> + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + { + const { renderedComponents } = await Profiler.takeRender(); - rerender({ variables: { id: "2" } }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + { + const { snapshot } = await Profiler.takeRender(); - expect(renders.frames[2]).toMatchObject({ - ...mocks[1].result, - networkStatus: NetworkStatus.ready, + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, error: undefined, + networkStatus: NetworkStatus.ready, }); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } - expect(renders.count).toBe(3); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toMatchObject([ - { - data: { character: { id: "1" } }, - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); - - it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { - interface Data { - character: { - id: string; - name: string; - }; - } - - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; + await act(() => user.click(screen.getByText("Refetch"))); - const partialQuery = gql` - query { - character { - id - } - } - `; - const mocks = [ - { - request: { query: fullQuery }, - result: { data: { character: { id: "1", name: "Doctor Strange" } } }, - }, - ]; - - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: DeepPartial; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + { + const { renderedComponents } = await Profiler.takeRender(); - const cache = new InMemoryCache(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); + { + const { snapshot } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, + expect(snapshot.result).toEqual({ + data: { primes: [13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [undefined, [13, 17, 19, 23, 29]], + ]); + } + }); +}); - function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } +describe("fetchMore", () => { + it("re-suspends when calling `fetchMore` with different variables", async () => { + const { query, link } = setupPaginatedCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function Parent() { - const [queryRef] = useBackgroundQuery(fullQuery, { - fetchPolicy: "network-only", - returnPartialData: true, - }); + function App() { + useTrackRenders(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query); - return ; - } + return ( + <> + + }> + + + + ); + } - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.frames.push({ data, networkStatus, error }); - renders.count++; - return ( - <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } + renderWithMocks(, { link, wrapper: Profiler }); - render(); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(renders.suspenseCount).toBe(1); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - await waitFor(() => { - expect(screen.getByTestId("character-name")).toHaveTextContent( - "Doctor Strange" - ); + { + const { snapshot } = await Profiler.takeRender(); + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - - expect(renders.count).toBe(1); - expect(renders.suspenseCount).toBe(1); + } - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); + await act(() => user.click(screen.getByText("Fetch more"))); - it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { - using _consoleSpy = spyOnConsole("warn"); - interface Data { - character: { - id: string; - name: string; - }; - } + { + const { renderedComponents } = await Profiler.takeRender(); - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const partialQuery = gql` - query { - character { - id - } - } - `; - const mocks = [ - { - request: { query: fullQuery }, - result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + { + const { snapshot } = await Profiler.takeRender(); + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 3, letter: "C" }, + { __typename: "Letter", position: 4, letter: "D" }, + ], }, - ]; - - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: DeepPartial; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; - - const cache = new InMemoryCache(); - - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } - - function Parent() { - const [queryRef] = useBackgroundQuery(fullQuery, { - fetchPolicy: "no-cache", - returnPartialData: true, - }); - - return ; - } - - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.frames.push({ data, networkStatus, error }); - renders.count++; - return ( - <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - render(); + it("properly uses `updateQuery` when calling `fetchMore`", async () => { + const { query, link } = setupPaginatedCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect(renders.suspenseCount).toBe(1); + function App() { + useTrackRenders(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query); - await waitFor(() => { - expect(screen.getByTestId("character-name")).toHaveTextContent( - "Doctor Strange" - ); - }); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + return ( + <> + + }> + + + + ); + } - expect(renders.count).toBe(1); - expect(renders.suspenseCount).toBe(1); + renderWithMocks(, { link, wrapper: Profiler }); - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { - using _consoleSpy = spyOnConsole("warn"); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const query: TypedDocumentNode = gql` - query UserQuery { - greeting - } - `; - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, + { + const { snapshot } = await Profiler.takeRender(); + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + ], }, - ]; + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - renderSuspenseHook( - () => - useBackgroundQuery(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - }), - { mocks } - ); + await act(() => user.click(screen.getByText("Fetch more"))); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith( - "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy." - ); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { - interface Data { - character: { - id: string; - name: string; - }; - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; + // TODO: Determine why we have this extra render here. + // Possibly related: https://github.com/apollographql/apollo-client/issues/11315 + { + const { snapshot } = await Profiler.takeRender(); - const partialQuery = gql` - query { - character { - id - } - } - `; - const mocks = [ - { - request: { query: fullQuery }, - result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + { __typename: "Letter", position: 3, letter: "C" }, + { __typename: "Letter", position: 4, letter: "D" }, + ], }, - ]; - - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: DeepPartial; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - const cache = new InMemoryCache(); + { + const { snapshot } = await Profiler.takeRender(); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + { __typename: "Letter", position: 3, letter: "C" }, + { __typename: "Letter", position: 4, letter: "D" }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - function App() { - return ( - - }> - - - - ); - } + it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + const { query, link } = setupPaginatedCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + letters: concatPagination(), + }, + }, + }, + }), + }); - function Parent() { - const [queryRef] = useBackgroundQuery(fullQuery, { - fetchPolicy: "cache-and-network", - returnPartialData: true, - }); + function App() { + useTrackRenders(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query); - return ; - } + return ( + <> + + }> + + + + ); + } - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.frames.push({ data, networkStatus, error }); - renders.count++; - return ( - <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } + renderWithClient(, { client, wrapper: Profiler }); - render(); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(renders.suspenseCount).toBe(0); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - // name is not present yet, since it's missing in partial data - expect(screen.getByTestId("character-name")).toHaveTextContent(""); - expect(screen.getByTestId("network-status")).toHaveTextContent("1"); // loading - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - await waitFor(() => { - expect(screen.getByTestId("character-name")).toHaveTextContent( - "Doctor Strange" - ); + { + const { snapshot } = await Profiler.takeRender(); + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + } - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(0); + await act(() => user.click(screen.getByText("Fetch more"))); - expect(renders.frames).toMatchObject([ - { - data: { character: { id: "1" } }, - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { - const partialQuery = gql` - query ($id: ID!) { - character(id: $id) { - id - } - } - `; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const cache = new InMemoryCache(); + // TODO: Determine why we have this extra render here. + // Possibly related: https://github.com/apollographql/apollo-client/issues/11315 + { + const { snapshot } = await Profiler.takeRender(); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - variables: { id: "1" }, + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + { __typename: "Letter", position: 3, letter: "C" }, + { __typename: "Letter", position: 4, letter: "D" }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const { renders, mocks, rerender } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - cache, - options: { - fetchPolicy: "cache-and-network", - returnPartialData: true, + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + { __typename: "Letter", position: 3, letter: "C" }, + { __typename: "Letter", position: 4, letter: "D" }, + ], }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - expect(renders.suspenseCount).toBe(0); + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + offset: number; + }; - rerender({ variables: { id: "2" } }); + interface Todo { + __typename: "Todo"; + id: string; + name: string; + completed: boolean; + } + interface Data { + todos: Todo[]; + } + const user = userEvent.setup(); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + const query: TypedDocumentNode = gql` + query TodosQuery($offset: Int!) { + todos(offset: $offset) { + id + name + completed + } + } + `; - expect(renders.count).toBe(3); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toMatchObject([ - { - data: { character: { id: "1" } }, - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, + const mocks: MockedResponse[] = [ + { + request: { query, variables: { offset: 0 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, }, - { - ...mocks[1].result, - networkStatus: NetworkStatus.ready, - error: undefined, + delay: 10, + }, + { + request: { query, variables: { offset: 1 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, }, - ]); - }); - - it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { - interface QueryData { - greeting: { - __typename: string; - message?: string; - recipient?: { - __typename: string; - name: string; - }; - }; - } - - const query: TypedDocumentNode = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; + delay: 10, + }, + ]; - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - // We are intentionally writing partial data to the cache. Supress console - // warnings to avoid unnecessary noise in the test. - { - using _consoleSpy = spyOnConsole("error"); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + todos: offsetLimitPagination(), }, }, - }); - } - - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: DeepPartial; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + }, + }), + }); - const client = new ApolloClient({ - link, - cache, + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query, { + variables: { offset: 0 }, }); - function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + Profiler.mergeSnapshot({ isPending }); - function Parent() { - const [queryRef] = useBackgroundQuery(query, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); + return ( + <> + + }> + + + + ); + } - return ; - } + renderWithClient(, { client, wrapper: Profiler }); - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.frames.push({ data, networkStatus, error }); - renders.count++; - return ( - <> -
{data.greeting?.message}
-
{data.greeting?.recipient?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } + { + const { renderedComponents } = await Profiler.takeRender(); - render(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(renders.suspenseCount).toBe(0); - expect(screen.getByTestId("recipient")).toHaveTextContent("Cached Alice"); - // message is not present yet, since it's missing in partial data - expect(screen.getByTestId("message")).toHaveTextContent(""); - expect(screen.getByTestId("network-status")).toHaveTextContent("1"); // loading - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + { + const { snapshot } = await Profiler.takeRender(); - link.simulateResult({ + expect(snapshot).toEqual({ + isPending: false, result: { data: { - greeting: { message: "Hello world", __typename: "Greeting" }, + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], }, - hasNext: true, + error: undefined, + networkStatus: NetworkStatus.ready, }, }); + } - await waitFor(() => { - expect(screen.getByTestId("message")).toHaveTextContent("Hello world"); - }); - expect(screen.getByTestId("recipient")).toHaveTextContent("Cached Alice"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + await act(() => user.click(screen.getByText("Load more"))); + + { + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + const { snapshot, renderedComponents } = await Profiler.takeRender(); - link.simulateResult({ + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: true, result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, }, - path: ["greeting"], - }, - ], - hasNext: false, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, }, }); + } - await waitFor(() => { - expect(screen.getByTestId("recipient").textContent).toEqual("Alice"); - }); - expect(screen.getByTestId("message")).toHaveTextContent("Hello world"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + // TODO: Determine why we have this extra render here. This should mimic + // the update in the next render where we see included in the + // rerendered components. + // Possibly related: https://github.com/apollographql/apollo-client/issues/11315 + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renders.count).toBe(3); - expect(renders.suspenseCount).toBe(0); - expect(renders.frames).toMatchObject([ - { + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], }, - networkStatus: NetworkStatus.loading, error: undefined, - }, - { - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, networkStatus: NetworkStatus.ready, - error: undefined, }, - { + }); + } + + { + // Eventually we should see the updated todos content once its done + // suspending. + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], }, - networkStatus: NetworkStatus.ready, error: undefined, + networkStatus: NetworkStatus.ready, }, - ]); - }); + }); + } + + await expect(Profiler).not.toRerender(); }); +}); - describe.skip("type tests", () => { - it("returns unknown when TData cannot be inferred", () => { - const query = gql` - query { - hello - } - `; +describe.skip("type tests", () => { + it("returns unknown when TData cannot be inferred", () => { + const query = gql` + query { + hello + } + `; - const [queryRef] = useBackgroundQuery(query); - const { data } = useReadQuery(queryRef); + const [queryRef] = useBackgroundQuery(query); + const { data } = useReadQuery(queryRef); - expectTypeOf(data).toEqualTypeOf(); - }); + expectTypeOf(data).toEqualTypeOf(); + }); - it("disallows wider variables type than specified", () => { - const { query } = useVariablesIntegrationTestCase(); + it("disallows wider variables type than specified", () => { + const { query } = setupVariablesCase(); - // @ts-expect-error should not allow wider TVariables type - useBackgroundQuery(query, { variables: { id: "1", foo: "bar" } }); + useBackgroundQuery(query, { + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, }); + }); - it("returns TData in default case", () => { - const { query } = useVariablesIntegrationTestCase(); - - const [inferredQueryRef] = useBackgroundQuery(query); - const { data: inferred } = useReadQuery(inferredQueryRef); - - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + it("returns TData in default case", () => { + const { query } = setupVariablesCase(); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query); + const [inferredQueryRef] = useBackgroundQuery(query); + const { data: inferred } = useReadQuery(inferredQueryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); - }); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query); - it('returns TData | undefined with errorPolicy: "ignore"', () => { - const { query } = useVariablesIntegrationTestCase(); + const { data: explicit } = useReadQuery(explicitQueryRef); - const [inferredQueryRef] = useBackgroundQuery(query, { - errorPolicy: "ignore", - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + it('returns TData | undefined with errorPolicy: "ignore"', () => { + const { query } = setupVariablesCase(); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - errorPolicy: "ignore", - }); + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: "ignore", + }); + const { data: inferred } = useReadQuery(inferredQueryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + errorPolicy: "ignore", }); - it('returns TData | undefined with errorPolicy: "all"', () => { - const { query } = useVariablesIntegrationTestCase(); - - const [inferredQueryRef] = useBackgroundQuery(query, { - errorPolicy: "all", - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + const { data: explicit } = useReadQuery(explicitQueryRef); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - const [explicitQueryRef] = useBackgroundQuery(query, { - errorPolicy: "all", - }); - const { data: explicit } = useReadQuery(explicitQueryRef); + it('returns TData | undefined with errorPolicy: "all"', () => { + const { query } = setupVariablesCase(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: "all", }); + const { data: inferred } = useReadQuery(inferredQueryRef); - it('returns TData with errorPolicy: "none"', () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); - const [inferredQueryRef] = useBackgroundQuery(query, { - errorPolicy: "none", - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + const [explicitQueryRef] = useBackgroundQuery(query, { + errorPolicy: "all", + }); + const { data: explicit } = useReadQuery(explicitQueryRef); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - const [explicitQueryRef] = useBackgroundQuery(query, { - errorPolicy: "none", - }); - const { data: explicit } = useReadQuery(explicitQueryRef); + it('returns TData with errorPolicy: "none"', () => { + const { query } = setupVariablesCase(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: "none", }); + const { data: inferred } = useReadQuery(inferredQueryRef); - it("returns DeepPartial with returnPartialData: true", () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); - const [inferredQueryRef] = useBackgroundQuery(query, { - returnPartialData: true, - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + const [explicitQueryRef] = useBackgroundQuery(query, { + errorPolicy: "none", + }); + const { data: explicit } = useReadQuery(explicitQueryRef); + + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - expectTypeOf(inferred).toEqualTypeOf>(); - expectTypeOf(inferred).not.toEqualTypeOf(); + it("returns DeepPartial with returnPartialData: true", () => { + const { query } = setupVariablesCase(); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: true, - }); + const [inferredQueryRef] = useBackgroundQuery(query, { + returnPartialData: true, + }); + const { data: inferred } = useReadQuery(inferredQueryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + expectTypeOf(inferred).toEqualTypeOf>(); + expectTypeOf(inferred).not.toEqualTypeOf(); - expectTypeOf(explicit).toEqualTypeOf>(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, }); - it("returns TData with returnPartialData: false", () => { - const { query } = useVariablesIntegrationTestCase(); + const { data: explicit } = useReadQuery(explicitQueryRef); - const [inferredQueryRef] = useBackgroundQuery(query, { - returnPartialData: false, - }); - const { data: inferred } = useReadQuery(inferredQueryRef); - - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf< - DeepPartial - >(); - - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: false, - }); + expectTypeOf(explicit).toEqualTypeOf>(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - const { data: explicit } = useReadQuery(explicitQueryRef); + it("returns TData with returnPartialData: false", () => { + const { query } = setupVariablesCase(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf< - DeepPartial - >(); + const [inferredQueryRef] = useBackgroundQuery(query, { + returnPartialData: false, }); + const { data: inferred } = useReadQuery(inferredQueryRef); - it("returns TData when passing an option that does not affect TData", () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf>(); - const [inferredQueryRef] = useBackgroundQuery(query, { - fetchPolicy: "no-cache", - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: false, + }); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf< - DeepPartial - >(); + const { data: explicit } = useReadQuery(explicitQueryRef); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - fetchPolicy: "no-cache", - }); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf>(); + }); - const { data: explicit } = useReadQuery(explicitQueryRef); + it("returns TData when passing an option that does not affect TData", () => { + const { query } = setupVariablesCase(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf< - DeepPartial - >(); + const [inferredQueryRef] = useBackgroundQuery(query, { + fetchPolicy: "no-cache", }); + const { data: inferred } = useReadQuery(inferredQueryRef); - it("handles combinations of options", () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf>(); - const [inferredPartialDataIgnoreQueryRef] = useBackgroundQuery(query, { - returnPartialData: true, - errorPolicy: "ignore", - }); - const { data: inferredPartialDataIgnore } = useReadQuery( - inferredPartialDataIgnoreQueryRef - ); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: "no-cache", + }); - expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< - DeepPartial | undefined - >(); - expectTypeOf( - inferredPartialDataIgnore - ).not.toEqualTypeOf(); - - const [explicitPartialDataIgnoreQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: true, - errorPolicy: "ignore", - }); + const { data: explicit } = useReadQuery(explicitQueryRef); - const { data: explicitPartialDataIgnore } = useReadQuery( - explicitPartialDataIgnoreQueryRef - ); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf>(); + }); - expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< - DeepPartial | undefined - >(); - expectTypeOf( - explicitPartialDataIgnore - ).not.toEqualTypeOf(); + it("handles combinations of options", () => { + const { query } = setupVariablesCase(); - const [inferredPartialDataNoneQueryRef] = useBackgroundQuery(query, { - returnPartialData: true, - errorPolicy: "none", - }); + const [inferredPartialDataIgnoreQueryRef] = useBackgroundQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); + const { data: inferredPartialDataIgnore } = useReadQuery( + inferredPartialDataIgnoreQueryRef + ); - const { data: inferredPartialDataNone } = useReadQuery( - inferredPartialDataNoneQueryRef - ); + expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf( + inferredPartialDataIgnore + ).not.toEqualTypeOf(); + + const [explicitPartialDataIgnoreQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); - expectTypeOf(inferredPartialDataNone).toEqualTypeOf< - DeepPartial - >(); - expectTypeOf( - inferredPartialDataNone - ).not.toEqualTypeOf(); - - const [explicitPartialDataNoneQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: true, - errorPolicy: "none", - }); + const { data: explicitPartialDataIgnore } = useReadQuery( + explicitPartialDataIgnoreQueryRef + ); - const { data: explicitPartialDataNone } = useReadQuery( - explicitPartialDataNoneQueryRef - ); + expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf( + explicitPartialDataIgnore + ).not.toEqualTypeOf(); - expectTypeOf(explicitPartialDataNone).toEqualTypeOf< - DeepPartial - >(); - expectTypeOf( - explicitPartialDataNone - ).not.toEqualTypeOf(); + const [inferredPartialDataNoneQueryRef] = useBackgroundQuery(query, { + returnPartialData: true, + errorPolicy: "none", }); - it("returns correct TData type when combined options that do not affect TData", () => { - const { query } = useVariablesIntegrationTestCase(); + const { data: inferredPartialDataNone } = useReadQuery( + inferredPartialDataNoneQueryRef + ); - const [inferredQueryRef] = useBackgroundQuery(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - errorPolicy: "none", - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + expectTypeOf(inferredPartialDataNone).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf( + inferredPartialDataNone + ).not.toEqualTypeOf(); + + const [explicitPartialDataNoneQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + errorPolicy: "none", + }); - expectTypeOf(inferred).toEqualTypeOf>(); - expectTypeOf(inferred).not.toEqualTypeOf(); + const { data: explicitPartialDataNone } = useReadQuery( + explicitPartialDataNoneQueryRef + ); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - errorPolicy: "none", - }); + expectTypeOf(explicitPartialDataNone).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf( + explicitPartialDataNone + ).not.toEqualTypeOf(); + }); - const { data: explicit } = useReadQuery(explicitQueryRef); + it("returns correct TData type when combined options that do not affect TData", () => { + const { query } = setupVariablesCase(); - expectTypeOf(explicit).toEqualTypeOf>(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [inferredQueryRef] = useBackgroundQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf>(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", }); - it("returns QueryReference | undefined when `skip` is present", () => { - const { query } = useVariablesIntegrationTestCase(); + const { data: explicit } = useReadQuery(explicitQueryRef); - const [inferredQueryRef] = useBackgroundQuery(query, { - skip: true, - }); + expectTypeOf(explicit).toEqualTypeOf>(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference | undefined - >(); - expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference - >(); - - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { skip: true }); - - expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference | undefined - >(); - expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference - >(); - - // TypeScript is too smart and using a `const` or `let` boolean variable - // for the `skip` option results in a false positive. Using an options - // object allows us to properly check for a dynamic case. - const options = { - skip: true, - }; + it("returns QueryReference | undefined when `skip` is present", () => { + const { query } = setupVariablesCase(); - const [dynamicQueryRef] = useBackgroundQuery(query, { - skip: options.skip, - }); + const [inferredQueryRef] = useBackgroundQuery(query, { + skip: true, + }); + + expectTypeOf(inferredQueryRef).toEqualTypeOf< + QueryReference | undefined + >(); + expectTypeOf(inferredQueryRef).not.toEqualTypeOf< + QueryReference + >(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { skip: true }); + + expectTypeOf(explicitQueryRef).toEqualTypeOf< + QueryReference | undefined + >(); + expectTypeOf(explicitQueryRef).not.toEqualTypeOf< + QueryReference + >(); + + // TypeScript is too smart and using a `const` or `let` boolean variable + // for the `skip` option results in a false positive. Using an options + // object allows us to properly check for a dynamic case. + const options = { + skip: true, + }; - expectTypeOf(dynamicQueryRef).toEqualTypeOf< - QueryReference | undefined - >(); - expectTypeOf(dynamicQueryRef).not.toEqualTypeOf< - QueryReference - >(); + const [dynamicQueryRef] = useBackgroundQuery(query, { + skip: options.skip, }); - it("returns `undefined` when using `skipToken` unconditionally", () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(dynamicQueryRef).toEqualTypeOf< + QueryReference | undefined + >(); + expectTypeOf(dynamicQueryRef).not.toEqualTypeOf< + QueryReference + >(); + }); + + it("returns `undefined` when using `skipToken` unconditionally", () => { + const { query } = setupVariablesCase(); - const [inferredQueryRef] = useBackgroundQuery(query, skipToken); + const [inferredQueryRef] = useBackgroundQuery(query, skipToken); - expectTypeOf(inferredQueryRef).toEqualTypeOf(); - expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference | undefined - >(); + expectTypeOf(inferredQueryRef).toEqualTypeOf(); + expectTypeOf(inferredQueryRef).not.toEqualTypeOf< + QueryReference | undefined + >(); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, skipToken); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, skipToken); - expectTypeOf(explicitQueryRef).toEqualTypeOf(); - expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference | undefined - >(); - }); + expectTypeOf(explicitQueryRef).toEqualTypeOf(); + expectTypeOf(explicitQueryRef).not.toEqualTypeOf< + QueryReference | undefined + >(); + }); - it("returns QueryReference | undefined when using conditional `skipToken`", () => { - const { query } = useVariablesIntegrationTestCase(); - const options = { - skip: true, - }; + it("returns QueryReference | undefined when using conditional `skipToken`", () => { + const { query } = setupVariablesCase(); + const options = { + skip: true, + }; - const [inferredQueryRef] = useBackgroundQuery( - query, - options.skip ? skipToken : undefined - ); + const [inferredQueryRef] = useBackgroundQuery( + query, + options.skip ? skipToken : undefined + ); - expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference | undefined - >(); - expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference - >(); - - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, options.skip ? skipToken : undefined); - - expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference | undefined - >(); - expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference - >(); - }); + expectTypeOf(inferredQueryRef).toEqualTypeOf< + QueryReference | undefined + >(); + expectTypeOf(inferredQueryRef).not.toEqualTypeOf< + QueryReference + >(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, options.skip ? skipToken : undefined); + + expectTypeOf(explicitQueryRef).toEqualTypeOf< + QueryReference | undefined + >(); + expectTypeOf(explicitQueryRef).not.toEqualTypeOf< + QueryReference + >(); + }); - it("returns QueryReference> | undefined when using `skipToken` with `returnPartialData`", () => { - const { query } = useVariablesIntegrationTestCase(); - const options = { - skip: true, - }; + it("returns QueryReference> | undefined when using `skipToken` with `returnPartialData`", () => { + const { query } = setupVariablesCase(); + const options = { + skip: true, + }; - const [inferredQueryRef] = useBackgroundQuery( - query, - options.skip ? skipToken : { returnPartialData: true } - ); + const [inferredQueryRef] = useBackgroundQuery( + query, + options.skip ? skipToken : { returnPartialData: true } + ); - expectTypeOf(inferredQueryRef).toEqualTypeOf< - | QueryReference, VariablesCaseVariables> - | undefined - >(); - expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference - >(); - - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, options.skip ? skipToken : { returnPartialData: true }); - - expectTypeOf(explicitQueryRef).toEqualTypeOf< - | QueryReference, VariablesCaseVariables> - | undefined - >(); - expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference - >(); - }); + expectTypeOf(inferredQueryRef).toEqualTypeOf< + | QueryReference, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(inferredQueryRef).not.toEqualTypeOf< + QueryReference + >(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, options.skip ? skipToken : { returnPartialData: true }); + + expectTypeOf(explicitQueryRef).toEqualTypeOf< + | QueryReference, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(explicitQueryRef).not.toEqualTypeOf< + QueryReference + >(); }); }); diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index a1ee0464f7e..a7f2dd72f43 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -1154,8 +1154,8 @@ test("resuspends when calling `fetchMore`", async () => { expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, ], }, error: undefined, @@ -1177,8 +1177,8 @@ test("resuspends when calling `fetchMore`", async () => { expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "C", position: 3 }, - { letter: "D", position: 4 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, ], }, error: undefined, @@ -1253,8 +1253,8 @@ test("properly uses `updateQuery` when calling `fetchMore`", async () => { expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, ], }, error: undefined, @@ -1276,10 +1276,10 @@ test("properly uses `updateQuery` when calling `fetchMore`", async () => { expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, - { letter: "C", position: 3 }, - { letter: "D", position: 4 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, ], }, error: undefined, @@ -1358,8 +1358,8 @@ test("properly uses cache field policies when calling `fetchMore` without `updat expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, ], }, error: undefined, @@ -1381,10 +1381,10 @@ test("properly uses cache field policies when calling `fetchMore` without `updat expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, - { letter: "C", position: 3 }, - { letter: "D", position: 4 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, ], }, error: undefined, @@ -1452,8 +1452,8 @@ test("paginates from queryRefs produced by useBackgroundQuery", async () => { expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, ], }, error: undefined, @@ -1475,8 +1475,8 @@ test("paginates from queryRefs produced by useBackgroundQuery", async () => { expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "C", position: 3 }, - { letter: "D", position: 4 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, ], }, error: undefined, @@ -1552,8 +1552,8 @@ test("paginates from queryRefs produced by useLoadableQuery", async () => { expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, ], }, error: undefined, @@ -1575,8 +1575,8 @@ test("paginates from queryRefs produced by useLoadableQuery", async () => { expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "C", position: 3 }, - { letter: "D", position: 4 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, ], }, error: undefined, @@ -1652,8 +1652,8 @@ test("`fetchMore` works with startTransition", async () => { expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, ], }, error: undefined, @@ -1672,8 +1672,8 @@ test("`fetchMore` works with startTransition", async () => { result: { data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, ], }, error: undefined, @@ -1691,8 +1691,8 @@ test("`fetchMore` works with startTransition", async () => { result: { data: { letters: [ - { letter: "C", position: 3 }, - { letter: "D", position: 4 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, ], }, error: undefined, @@ -1789,8 +1789,8 @@ test("`fetchMore` works with startTransition from useBackgroundQuery and useQuer expect(snapshot.result).toEqual({ data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, ], }, error: undefined, @@ -1810,8 +1810,8 @@ test("`fetchMore` works with startTransition from useBackgroundQuery and useQuer result: { data: { letters: [ - { letter: "A", position: 1 }, - { letter: "B", position: 2 }, + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, ], }, error: undefined, @@ -1830,8 +1830,8 @@ test("`fetchMore` works with startTransition from useBackgroundQuery and useQuer result: { data: { letters: [ - { letter: "C", position: 3 }, - { letter: "D", position: 4 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, ], }, error: undefined, @@ -1852,8 +1852,8 @@ test("`fetchMore` works with startTransition from useBackgroundQuery and useQuer result: { data: { letters: [ - { letter: "C", position: 3 }, - { letter: "D", position: 4 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, ], }, error: undefined, @@ -1872,8 +1872,8 @@ test("`fetchMore` works with startTransition from useBackgroundQuery and useQuer result: { data: { letters: [ - { letter: "E", position: 5 }, - { letter: "F", position: 6 }, + { __typename: "Letter", letter: "E", position: 5 }, + { __typename: "Letter", letter: "F", position: 6 }, ], }, error: undefined, diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts index 411099d3615..637c2a7bec4 100644 --- a/src/testing/internal/scenarios/index.ts +++ b/src/testing/internal/scenarios/index.ts @@ -88,9 +88,11 @@ export function setupPaginatedCase() { } `; - const data = "ABCDEFGHIJKLMNOPQRSTUV" - .split("") - .map((letter, index) => ({ letter, position: index + 1 })); + const data = "ABCDEFGHIJKLMNOPQRSTUV".split("").map((letter, index) => ({ + __typename: "Letter", + letter, + position: index + 1, + })); const link = new ApolloLink((operation) => { const { offset = 0, limit = 2 } = operation.variables; From 6d46ab930a5e9bd5cae153d3b75b8966784fcd4e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 19 Dec 2023 08:56:17 -0700 Subject: [PATCH 69/90] Remove `retain` call from `useBackgroundQuery` to allow for auto disposal (#11438) --- .changeset/wise-news-grab.md | 7 + .size-limits.json | 2 +- .../__tests__/useBackgroundQuery.test.tsx | 291 ++++++++++++++++++ src/react/hooks/useBackgroundQuery.ts | 2 - 4 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 .changeset/wise-news-grab.md diff --git a/.changeset/wise-news-grab.md b/.changeset/wise-news-grab.md new file mode 100644 index 00000000000..83eafb1375f --- /dev/null +++ b/.changeset/wise-news-grab.md @@ -0,0 +1,7 @@ +--- +'@apollo/client': minor +--- + +Remove the need to call `retain` from `useBackgroundQuery` since `useReadQuery` will now retain the query. This means that a `queryRef` that is not consumed by `useReadQuery` within the given `autoDisposeTimeoutMs` will now be auto diposed for you. + +Thanks to [#11412](https://github.com/apollographql/apollo-client/pull/11412), disposed query refs will be automatically resubscribed to the query when consumed by `useReadQuery` after it has been disposed. diff --git a/.size-limits.json b/.size-limits.json index 7a4493b82cf..fa4846d0655 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39135, + "dist/apollo-client.min.cjs": 39130, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32651 } diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 006cc0c876e..fbd7c3dd973 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -23,6 +23,7 @@ import { MockSubscriptionLink, mockSingleLink, MockedProvider, + wait, } from "../../../testing"; import { concatPagination, @@ -54,6 +55,10 @@ import { useTrackRenders, } from "../../../testing/internal"; +afterEach(() => { + jest.useRealTimers(); +}); + function createDefaultTrackedComponents< Snapshot extends { result: UseReadQueryResult | null }, TData = Snapshot["result"] extends UseReadQueryResult | null ? @@ -155,6 +160,292 @@ it("fetches a simple query with minimal config", async () => { await expect(Profiler).not.toRerender({ timeout: 50 }); }); +it("tears down the query on unmount", async () => { + const { query, mocks } = setupSimpleCase(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); + + return ( + }> + + + ); + } + + const { unmount } = renderWithClient(, { client, wrapper: Profiler }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + unmount(); + + await wait(0); + + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +it("auto disposes of the queryRef if not used within timeout", async () => { + jest.useFakeTimers(); + const { query } = setupSimpleCase(); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ link, cache: new InMemoryCache() }); + + const { result } = renderHook(() => useBackgroundQuery(query, { client })); + + const [queryRef] = result.current; + + expect(queryRef).not.toBeDisposed(); + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + await act(() => { + link.simulateResult({ result: { data: { greeting: "Hello" } } }, true); + // Ensure simulateResult will deliver the result since its wrapped with + // setTimeout + jest.advanceTimersByTime(10); + }); + + jest.advanceTimersByTime(30_000); + + expect(queryRef).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +it("auto disposes of the queryRef if not used within configured timeout", async () => { + jest.useFakeTimers(); + const { query } = setupSimpleCase(); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + defaultOptions: { + react: { + suspense: { + autoDisposeTimeoutMs: 5000, + }, + }, + }, + }); + + const { result } = renderHook(() => useBackgroundQuery(query, { client })); + + const [queryRef] = result.current; + + expect(queryRef).not.toBeDisposed(); + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + await act(() => { + link.simulateResult({ result: { data: { greeting: "Hello" } } }, true); + // Ensure simulateResult will deliver the result since its wrapped with + // setTimeout + jest.advanceTimersByTime(10); + }); + + jest.advanceTimersByTime(5000); + + expect(queryRef).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +it("will resubscribe after disposed when mounting useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + defaultOptions: { + react: { + suspense: { + // Set this to something really low to avoid fake timers + autoDisposeTimeoutMs: 20, + }, + }, + }, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(false); + const [queryRef] = useBackgroundQuery(query); + + return ( + <> + + }> + {show && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } + + // Wait long enough for auto dispose to kick in + await wait(50); + + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + await act(() => user.click(screen.getByText("Toggle"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ + query, + data: { greeting: "Hello again" }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it("auto resubscribes when mounting useReadQuery after naturally disposed by useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + const [queryRef] = useBackgroundQuery(query); + + return ( + <> + + }> + {show && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + await act(() => user.click(toggleButton)); + + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ + query, + data: { greeting: "Hello again" }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + it("allows the client to be overridden", async () => { const { query } = setupSimpleCase(); diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index af5058b5cac..ab9105d4243 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -219,8 +219,6 @@ export function useBackgroundQuery< updateWrappedQueryRef(wrappedQueryRef, promise); } - React.useEffect(() => queryRef.retain(), [queryRef]); - const fetchMore: FetchMoreFunction = React.useCallback( (options) => { const promise = queryRef.fetchMore(options as FetchMoreQueryOptions); From 62f3b6d0e89611e27d9f29812ee60e5db5963fd6 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 20 Dec 2023 11:55:23 +0100 Subject: [PATCH 70/90] Simplify RetryLink, fix potential memory leak (#11424) fixes #11393 --- .changeset/curvy-seas-hope.md | 13 ++ .../batch-http/__tests__/batchHttpLink.ts | 3 + src/link/http/__tests__/HttpLink.ts | 1 + src/link/retry/__tests__/retryLink.ts | 21 ++- src/link/retry/retryLink.ts | 121 +++--------------- 5 files changed, 51 insertions(+), 108 deletions(-) create mode 100644 .changeset/curvy-seas-hope.md diff --git a/.changeset/curvy-seas-hope.md b/.changeset/curvy-seas-hope.md new file mode 100644 index 00000000000..65491ac6318 --- /dev/null +++ b/.changeset/curvy-seas-hope.md @@ -0,0 +1,13 @@ +--- +"@apollo/client": minor +--- + +Simplify RetryLink, fix potential memory leak + +Historically, `RetryLink` would keep a `values` array of all previous values, +in case the operation would get an additional subscriber at a later point in time. +In practice, this could lead to a memory leak (#11393) and did not serve any +further purpose, as the resulting observable would only be subscribed to by +Apollo Client itself, and only once - it would be wrapped in a `Concast` before +being exposed to the user, and that `Concast` would handle subscribers on its +own. diff --git a/src/link/batch-http/__tests__/batchHttpLink.ts b/src/link/batch-http/__tests__/batchHttpLink.ts index 544f44c304f..6dea1805b89 100644 --- a/src/link/batch-http/__tests__/batchHttpLink.ts +++ b/src/link/batch-http/__tests__/batchHttpLink.ts @@ -524,6 +524,9 @@ describe("SharedHttpTest", () => { expect(subscriber.next).toHaveBeenCalledTimes(2); expect(subscriber.complete).toHaveBeenCalledTimes(2); expect(subscriber.error).not.toHaveBeenCalled(); + // only one call because batchHttpLink can handle more than one subscriber + // without starting a new request + expect(fetchMock.calls().length).toBe(1); resolve(); }, 50); }); diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts index b2ce5308cfd..5c02986b279 100644 --- a/src/link/http/__tests__/HttpLink.ts +++ b/src/link/http/__tests__/HttpLink.ts @@ -634,6 +634,7 @@ describe("HttpLink", () => { expect(subscriber.next).toHaveBeenCalledTimes(2); expect(subscriber.complete).toHaveBeenCalledTimes(2); expect(subscriber.error).not.toHaveBeenCalled(); + expect(fetchMock.calls().length).toBe(2); resolve(); }, 50); }); diff --git a/src/link/retry/__tests__/retryLink.ts b/src/link/retry/__tests__/retryLink.ts index 3f5413e5b39..b9f3e14440d 100644 --- a/src/link/retry/__tests__/retryLink.ts +++ b/src/link/retry/__tests__/retryLink.ts @@ -92,7 +92,12 @@ describe("RetryLink", () => { expect(unsubscribeStub).toHaveBeenCalledTimes(1); }); - it("supports multiple subscribers to the same request", async () => { + it("multiple subscribers will trigger multiple requests", async () => { + const subscriber = { + next: jest.fn(console.log), + error: jest.fn(console.error), + complete: jest.fn(console.info), + }; const retry = new RetryLink({ delay: { initial: 1 }, attempts: { max: 5 }, @@ -102,13 +107,19 @@ describe("RetryLink", () => { stub.mockReturnValueOnce(fromError(standardError)); stub.mockReturnValueOnce(fromError(standardError)); stub.mockReturnValueOnce(Observable.of(data)); + stub.mockReturnValueOnce(fromError(standardError)); + stub.mockReturnValueOnce(fromError(standardError)); + stub.mockReturnValueOnce(Observable.of(data)); const link = ApolloLink.from([retry, stub]); const observable = execute(link, { query }); - const [result1, result2] = (await waitFor(observable, observable)) as any; - expect(result1.values).toEqual([data]); - expect(result2.values).toEqual([data]); - expect(stub).toHaveBeenCalledTimes(3); + observable.subscribe(subscriber); + observable.subscribe(subscriber); + await new Promise((resolve) => setTimeout(resolve, 3500)); + expect(subscriber.next).toHaveBeenNthCalledWith(1, data); + expect(subscriber.next).toHaveBeenNthCalledWith(2, data); + expect(subscriber.complete).toHaveBeenCalledTimes(2); + expect(stub).toHaveBeenCalledTimes(6); }); it("retries independently for concurrent requests", async () => { diff --git a/src/link/retry/retryLink.ts b/src/link/retry/retryLink.ts index d44a382500c..cde2dd2ea9c 100644 --- a/src/link/retry/retryLink.ts +++ b/src/link/retry/retryLink.ts @@ -1,14 +1,12 @@ import type { Operation, FetchResult, NextLink } from "../core/index.js"; import { ApolloLink } from "../core/index.js"; -import type { - Observer, - ObservableSubscription, -} from "../../utilities/index.js"; +import type { ObservableSubscription } from "../../utilities/index.js"; import { Observable } from "../../utilities/index.js"; import type { DelayFunction, DelayFunctionOptions } from "./delayFunction.js"; import { buildDelayFunction } from "./delayFunction.js"; import type { RetryFunction, RetryFunctionOptions } from "./retryFunction.js"; import { buildRetryFunction } from "./retryFunction.js"; +import type { SubscriptionObserver } from "zen-observable-ts"; export namespace RetryLink { export interface Options { @@ -27,78 +25,18 @@ export namespace RetryLink { /** * Tracking and management of operations that may be (or currently are) retried. */ -class RetryableOperation { +class RetryableOperation { private retryCount: number = 0; - private values: any[] = []; - private error: any; - private complete = false; - private canceled = false; - private observers: (Observer | null)[] = []; private currentSubscription: ObservableSubscription | null = null; private timerId: number | undefined; constructor( + private observer: SubscriptionObserver, private operation: Operation, - private nextLink: NextLink, + private forward: NextLink, private delayFor: DelayFunction, private retryIf: RetryFunction - ) {} - - /** - * Register a new observer for this operation. - * - * If the operation has previously emitted other events, they will be - * immediately triggered for the observer. - */ - public subscribe(observer: Observer) { - if (this.canceled) { - throw new Error( - `Subscribing to a retryable link that was canceled is not supported` - ); - } - this.observers.push(observer); - - // If we've already begun, catch this observer up. - for (const value of this.values) { - observer.next!(value); - } - - if (this.complete) { - observer.complete!(); - } else if (this.error) { - observer.error!(this.error); - } - } - - /** - * Remove a previously registered observer from this operation. - * - * If no observers remain, the operation will stop retrying, and unsubscribe - * from its downstream link. - */ - public unsubscribe(observer: Observer) { - const index = this.observers.indexOf(observer); - if (index < 0) { - throw new Error( - `RetryLink BUG! Attempting to unsubscribe unknown observer!` - ); - } - // Note that we are careful not to change the order of length of the array, - // as we are often mid-iteration when calling this method. - this.observers[index] = null; - - // If this is the last observer, we're done. - if (this.observers.every((o) => o === null)) { - this.cancel(); - } - } - - /** - * Start the initial request. - */ - public start() { - if (this.currentSubscription) return; // Already started. - + ) { this.try(); } @@ -112,33 +50,16 @@ class RetryableOperation { clearTimeout(this.timerId); this.timerId = undefined; this.currentSubscription = null; - this.canceled = true; } private try() { - this.currentSubscription = this.nextLink(this.operation).subscribe({ - next: this.onNext, + this.currentSubscription = this.forward(this.operation).subscribe({ + next: this.observer.next.bind(this.observer), error: this.onError, - complete: this.onComplete, + complete: this.observer.complete.bind(this.observer), }); } - private onNext = (value: any) => { - this.values.push(value); - for (const observer of this.observers) { - if (!observer) continue; - observer.next!(value); - } - }; - - private onComplete = () => { - this.complete = true; - for (const observer of this.observers) { - if (!observer) continue; - observer.complete!(); - } - }; - private onError = async (error: any) => { this.retryCount += 1; @@ -153,11 +74,7 @@ class RetryableOperation { return; } - this.error = error; - for (const observer of this.observers) { - if (!observer) continue; - observer.error!(error); - } + this.observer.error(error); }; private scheduleRetry(delay: number) { @@ -189,18 +106,16 @@ export class RetryLink extends ApolloLink { operation: Operation, nextLink: NextLink ): Observable { - const retryable = new RetryableOperation( - operation, - nextLink, - this.delayFor, - this.retryIf - ); - retryable.start(); - return new Observable((observer) => { - retryable.subscribe(observer); + const retryable = new RetryableOperation( + observer, + operation, + nextLink, + this.delayFor, + this.retryIf + ); return () => { - retryable.unsubscribe(observer); + retryable.cancel(); }; }); } From 14edebebefb7634c32b921d02c1c85c6c8737989 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 20 Dec 2023 12:36:00 +0100 Subject: [PATCH 71/90] ObservableQuery: only report results for the current variables (#11078) * Add reproduction * ObservableQuery: only report results for the current variables * changeset * chores * add tests directly to `ObservableStream` * adjust fetchPolicy * add failing tests for missing cache write * remove unrelated test part * Update smooth-plums-shout.md * Clean up Prettier, Size-limit, and Api-Extractor * review suggestions --------- Co-authored-by: Jan Amann Co-authored-by: phryneas --- .changeset/smooth-plums-shout.md | 5 + .size-limits.json | 4 +- src/__tests__/mutationResults.ts | 4 +- src/core/ObservableQuery.ts | 12 +- src/core/__tests__/ObservableQuery.ts | 93 ++++++++++++++ src/react/hooks/__tests__/useQuery.test.tsx | 135 +++++++++++++++++++- src/testing/internal/ObservableStream.ts | 7 +- 7 files changed, 249 insertions(+), 11 deletions(-) create mode 100644 .changeset/smooth-plums-shout.md diff --git a/.changeset/smooth-plums-shout.md b/.changeset/smooth-plums-shout.md new file mode 100644 index 00000000000..909e07ede8f --- /dev/null +++ b/.changeset/smooth-plums-shout.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +ObservableQuery: prevent reporting results of previous queries if the variables changed since diff --git a/.size-limits.json b/.size-limits.json index fa4846d0655..f78538c39a5 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39130, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32651 + "dist/apollo-client.min.cjs": 39136, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32663 } diff --git a/src/__tests__/mutationResults.ts b/src/__tests__/mutationResults.ts index d7656a9232d..aa9a6183937 100644 --- a/src/__tests__/mutationResults.ts +++ b/src/__tests__/mutationResults.ts @@ -1186,8 +1186,6 @@ describe("mutation results", () => { subscribeAndCount(reject, watchedQuery, (count, result) => { if (count === 1) { - expect(result.data).toEqual({ echo: "a" }); - } else if (count === 2) { expect(result.data).toEqual({ echo: "b" }); client.mutate({ mutation: resetMutation, @@ -1197,7 +1195,7 @@ describe("mutation results", () => { }, }, }); - } else if (count === 3) { + } else if (count === 2) { expect(result.data).toEqual({ echo: "0" }); resolve(); } diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 5cd6189b84d..03418c0e1d9 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -906,12 +906,16 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, const { concast, fromLink } = this.fetch(options, newNetworkStatus, query); const observer: Observer> = { next: (result) => { - finishWaitingForOwnResult(); - this.reportResult(result, variables); + if (equal(this.variables, variables)) { + finishWaitingForOwnResult(); + this.reportResult(result, variables); + } }, error: (error) => { - finishWaitingForOwnResult(); - this.reportError(error, variables); + if (equal(this.variables, variables)) { + finishWaitingForOwnResult(); + this.reportError(error, variables); + } }, }; diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index add7b8a61ee..d25765e9c9b 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -34,6 +34,7 @@ import wrap from "../../testing/core/wrap"; import { resetStore } from "./QueryManager"; import { SubscriptionObserver } from "zen-observable-ts"; import { waitFor } from "@testing-library/react"; +import { ObservableStream } from "../../testing/internal"; export const mockFetchQuery = (queryManager: QueryManager) => { const fetchConcastWithInfo = queryManager["fetchConcastWithInfo"]; @@ -1086,6 +1087,98 @@ describe("ObservableQuery", () => { } ); + it("calling refetch with different variables before the query itself resolved will only yield the result for the new variables", async () => { + const observers: SubscriptionObserver>[] = []; + const queryManager = new QueryManager({ + cache: new InMemoryCache(), + link: new ApolloLink((operation, forward) => { + return new Observable((observer) => { + observers.push(observer); + }); + }), + }); + const observableQuery = queryManager.watchQuery({ + query, + variables: { id: 1 }, + }); + const stream = new ObservableStream(observableQuery); + + observableQuery.refetch({ id: 2 }); + + observers[0].next({ data: dataOne }); + observers[0].complete(); + + observers[1].next({ data: dataTwo }); + observers[1].complete(); + + { + const result = await stream.takeNext(); + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: dataTwo, + }); + } + expect(stream.take()).rejects.toThrow(/Timeout/i); + }); + + it("calling refetch multiple times with different variables will return only results for the most recent variables", async () => { + const observers: SubscriptionObserver>[] = []; + const queryManager = new QueryManager({ + cache: new InMemoryCache(), + link: new ApolloLink((operation, forward) => { + return new Observable((observer) => { + observers.push(observer); + }); + }), + }); + const observableQuery = queryManager.watchQuery({ + query, + variables: { id: 1 }, + }); + const stream = new ObservableStream(observableQuery); + + observers[0].next({ data: dataOne }); + observers[0].complete(); + + { + const result = await stream.takeNext(); + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: dataOne, + }); + } + + observableQuery.refetch({ id: 2 }); + observableQuery.refetch({ id: 3 }); + + observers[1].next({ data: dataTwo }); + observers[1].complete(); + + observers[2].next({ + data: { + people_one: { + name: "SomeOneElse", + }, + }, + }); + observers[2].complete(); + + { + const result = await stream.takeNext(); + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + people_one: { + name: "SomeOneElse", + }, + }, + }); + } + }); + itAsync( "calls fetchRequest with fetchPolicy `no-cache` when using `no-cache` fetch policy", (resolve, reject) => { diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 2e6f1e3a125..938781a9890 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, ReactNode, useEffect, useState } from "react"; +import React, { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { DocumentNode, GraphQLError } from "graphql"; import gql from "graphql-tag"; import { act } from "react-dom/test-utils"; @@ -27,6 +27,7 @@ import { QueryResult } from "../../types/types"; import { useQuery } from "../useQuery"; import { useMutation } from "../useMutation"; import { profileHook, spyOnConsole } from "../../../testing/internal"; +import { useApolloClient } from "../useApolloClient"; describe("useQuery Hook", () => { describe("General use", () => { @@ -4494,6 +4495,138 @@ describe("useQuery Hook", () => { }); }); }); + + it("keeps cache consistency when a call to refetchQueries is interrupted with another query caused by changing variables and the second query returns before the first one", async () => { + const CAR_QUERY_BY_ID = gql` + query Car($id: Int) { + car(id: $id) { + make + model + } + } + `; + + const mocks = { + 1: [ + { + car: { + make: "Audi", + model: "A4", + __typename: "Car", + }, + }, + { + car: { + make: "Audi", + model: "A3", // Changed + __typename: "Car", + }, + }, + ], + 2: [ + { + car: { + make: "Audi", + model: "RS8", + __typename: "Car", + }, + }, + ], + }; + + const link = new ApolloLink( + (operation) => + new Observable((observer) => { + if (operation.variables.id === 1) { + // Queries for this ID return after a delay + setTimeout(() => { + const data = mocks[1].splice(0, 1).pop(); + observer.next({ data }); + observer.complete(); + }, 100); + } else if (operation.variables.id === 2) { + // Queries for this ID return immediately + const data = mocks[2].splice(0, 1).pop(); + observer.next({ data }); + observer.complete(); + } else { + observer.error(new Error("Unexpected query")); + } + }) + ); + + const hookResponse = jest.fn().mockReturnValue(null); + + function Component({ children, id }: any) { + const result = useQuery(CAR_QUERY_BY_ID, { + variables: { id }, + notifyOnNetworkStatusChange: true, + fetchPolicy: "network-only", + }); + const client = useApolloClient(); + const hasRefetchedRef = useRef(false); + + useEffect(() => { + if ( + result.networkStatus === NetworkStatus.ready && + !hasRefetchedRef.current + ) { + client.reFetchObservableQueries(); + hasRefetchedRef.current = true; + } + }, [result.networkStatus]); + + return children(result); + } + + const { rerender } = render( + {hookResponse}, + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await waitFor(() => { + // Resolves as soon as reFetchObservableQueries is + // called, but before the result is returned + expect(hookResponse).toHaveBeenCalledTimes(3); + }); + + rerender({hookResponse}); + + await waitFor(() => { + // All results are returned + expect(hookResponse).toHaveBeenCalledTimes(5); + }); + + expect(hookResponse.mock.calls.map((call) => call[0].data)).toEqual([ + undefined, + { + car: { + __typename: "Car", + make: "Audi", + model: "A4", + }, + }, + { + car: { + __typename: "Car", + make: "Audi", + model: "A4", + }, + }, + undefined, + { + car: { + __typename: "Car", + make: "Audi", + model: "RS8", + }, + }, + ]); + }); }); describe("Callbacks", () => { diff --git a/src/testing/internal/ObservableStream.ts b/src/testing/internal/ObservableStream.ts index f0416692331..b19be0d469b 100644 --- a/src/testing/internal/ObservableStream.ts +++ b/src/testing/internal/ObservableStream.ts @@ -29,6 +29,7 @@ async function* observableToAsyncEventIterator(observable: Observable) { (error) => resolveNext({ type: "error", error }), () => resolveNext({ type: "complete" }) ); + yield "initialization value" as unknown as Promise>; while (true) { yield promises.shift()!; @@ -54,7 +55,11 @@ class IteratorStream { export class ObservableStream extends IteratorStream> { constructor(observable: Observable) { - super(observableToAsyncEventIterator(observable)); + const iterator = observableToAsyncEventIterator(observable); + // we need to call next() once to start the generator so we immediately subscribe. + // the first value is always "initialization value" which we don't care about + iterator.next(); + super(iterator); } async takeNext(options?: TakeOptions): Promise { From ff5a332ff8b190c418df25371e36719d70061ebe Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 20 Dec 2023 19:10:49 +0100 Subject: [PATCH 72/90] add deprecation warnings for HOC and Render Prop docblocks (#11443) * add deprecation warnings for HOC docblocks * changeset * api-extractor * also add render prop components * api report --- .api-reports/api-report-react_components.md | 6 +++--- .api-reports/api-report-react_hoc.md | 10 +++++----- .changeset/chatty-comics-yawn.md | 8 ++++++++ src/react/components/Mutation.tsx | 6 ++++++ src/react/components/Query.tsx | 6 ++++++ src/react/components/Subscription.tsx | 6 ++++++ src/react/hoc/graphql.tsx | 5 +++++ src/react/hoc/mutation-hoc.tsx | 5 +++++ src/react/hoc/query-hoc.tsx | 5 +++++ src/react/hoc/subscription-hoc.tsx | 5 +++++ src/react/hoc/withApollo.tsx | 5 +++++ 11 files changed, 59 insertions(+), 8 deletions(-) create mode 100644 .changeset/chatty-comics-yawn.md diff --git a/.api-reports/api-report-react_components.md b/.api-reports/api-report-react_components.md index 2b4d74e4b5e..0775e0c9cbb 100644 --- a/.api-reports/api-report-react_components.md +++ b/.api-reports/api-report-react_components.md @@ -946,7 +946,7 @@ type Modifiers = Record> = Partia [FieldName in keyof T]: Modifier>>; }>; -// @public (undocumented) +// @public @deprecated (undocumented) export function Mutation(props: MutationComponentOptions): ReactTypes.JSX.Element | null; // @public (undocumented) @@ -1218,7 +1218,7 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; -// @public (undocumented) +// @public @deprecated (undocumented) export function Query(props: QueryComponentOptions): ReactTypes.JSX.Element | null; // @public (undocumented) @@ -1624,7 +1624,7 @@ type SubscribeToMoreOptions(props: SubscriptionComponentOptions): ReactTypes.JSX.Element | null; // @public (undocumented) diff --git a/.api-reports/api-report-react_hoc.md b/.api-reports/api-report-react_hoc.md index 40b988534e5..ca318274946 100644 --- a/.api-reports/api-report-react_hoc.md +++ b/.api-reports/api-report-react_hoc.md @@ -756,7 +756,7 @@ const getApolloClientMemoryInternals: (() => { }; }) | undefined; -// @public (undocumented) +// @public @deprecated (undocumented) export function graphql> & Partial>>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; // @public (undocumented) @@ -1674,7 +1674,7 @@ interface WatchQueryOptions(WrappedComponent: ReactTypes.ComponentType>>, operationOptions?: OperationOption): ReactTypes.ComponentClass>; // @public (undocumented) @@ -1682,13 +1682,13 @@ export type WithApolloClient

= P & { client?: ApolloClient; }; -// @public (undocumented) +// @public @deprecated (undocumented) export function withMutation = {}, TGraphQLVariables extends OperationVariables = {}, TChildProps = MutateProps, TContext extends Record = DefaultContext, TCache extends ApolloCache = ApolloCache>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; -// @public (undocumented) +// @public @deprecated (undocumented) export function withQuery = Record, TData extends object = {}, TGraphQLVariables extends object = {}, TChildProps extends object = DataProps>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; -// @public (undocumented) +// @public @deprecated (undocumented) export function withSubscription>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; // Warnings were encountered during analysis: diff --git a/.changeset/chatty-comics-yawn.md b/.changeset/chatty-comics-yawn.md new file mode 100644 index 00000000000..b50a939eda2 --- /dev/null +++ b/.changeset/chatty-comics-yawn.md @@ -0,0 +1,8 @@ +--- +"@apollo/client": patch +--- + +Adds a deprecation warning to the HOC and render prop APIs. + +The HOC and render prop APIs have already been deprecated since 2020, +but we previously didn't have a @deprecated tag in the DocBlocks. diff --git a/src/react/components/Mutation.tsx b/src/react/components/Mutation.tsx index 8dca6889f7d..ff122a7e648 100644 --- a/src/react/components/Mutation.tsx +++ b/src/react/components/Mutation.tsx @@ -5,6 +5,12 @@ import type { OperationVariables } from "../../core/index.js"; import type { MutationComponentOptions } from "./types.js"; import { useMutation } from "../hooks/index.js"; +/** + * @deprecated + * Official support for React Apollo render prop components ended in March 2020. + * This library is still included in the `@apollo/client` package, + * but it no longer receives feature updates or bug fixes. + */ export function Mutation( props: MutationComponentOptions ): ReactTypes.JSX.Element | null { diff --git a/src/react/components/Query.tsx b/src/react/components/Query.tsx index 119696f3973..428207784c7 100644 --- a/src/react/components/Query.tsx +++ b/src/react/components/Query.tsx @@ -5,6 +5,12 @@ import type { OperationVariables } from "../../core/index.js"; import type { QueryComponentOptions } from "./types.js"; import { useQuery } from "../hooks/index.js"; +/** + * @deprecated + * Official support for React Apollo render prop components ended in March 2020. + * This library is still included in the `@apollo/client` package, + * but it no longer receives feature updates or bug fixes. + */ export function Query< TData = any, TVariables extends OperationVariables = OperationVariables, diff --git a/src/react/components/Subscription.tsx b/src/react/components/Subscription.tsx index 76dda1a6241..59d694156a5 100644 --- a/src/react/components/Subscription.tsx +++ b/src/react/components/Subscription.tsx @@ -5,6 +5,12 @@ import type { OperationVariables } from "../../core/index.js"; import type { SubscriptionComponentOptions } from "./types.js"; import { useSubscription } from "../hooks/index.js"; +/** + * @deprecated + * Official support for React Apollo render prop components ended in March 2020. + * This library is still included in the `@apollo/client` package, + * but it no longer receives feature updates or bug fixes. + */ export function Subscription< TData = any, TVariables extends OperationVariables = OperationVariables, diff --git a/src/react/hoc/graphql.tsx b/src/react/hoc/graphql.tsx index 1a770aae04e..181bb5a951e 100644 --- a/src/react/hoc/graphql.tsx +++ b/src/react/hoc/graphql.tsx @@ -8,6 +8,11 @@ import { withSubscription } from "./subscription-hoc.js"; import type { OperationOption, DataProps, MutateProps } from "./types.js"; import type { OperationVariables } from "../../core/index.js"; +/** + * @deprecated + * Official support for React Apollo higher order components ended in March 2020. + * This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. + */ export function graphql< TProps extends TGraphQLVariables | {} = {}, TData extends object = {}, diff --git a/src/react/hoc/mutation-hoc.tsx b/src/react/hoc/mutation-hoc.tsx index 472cf783106..2620f46ff81 100644 --- a/src/react/hoc/mutation-hoc.tsx +++ b/src/react/hoc/mutation-hoc.tsx @@ -21,6 +21,11 @@ import { import type { OperationOption, OptionProps, MutateProps } from "./types.js"; import type { ApolloCache } from "../../core/index.js"; +/** + * @deprecated + * Official support for React Apollo higher order components ended in March 2020. + * This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. + */ export function withMutation< TProps extends TGraphQLVariables | {} = {}, TData extends Record = {}, diff --git a/src/react/hoc/query-hoc.tsx b/src/react/hoc/query-hoc.tsx index 935645c5e44..cdefd397916 100644 --- a/src/react/hoc/query-hoc.tsx +++ b/src/react/hoc/query-hoc.tsx @@ -15,6 +15,11 @@ import { } from "./hoc-utils.js"; import type { OperationOption, OptionProps, DataProps } from "./types.js"; +/** + * @deprecated + * Official support for React Apollo higher order components ended in March 2020. + * This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. + */ export function withQuery< TProps extends TGraphQLVariables | Record = Record, TData extends object = {}, diff --git a/src/react/hoc/subscription-hoc.tsx b/src/react/hoc/subscription-hoc.tsx index f3218a00a0e..bb11060aaf7 100644 --- a/src/react/hoc/subscription-hoc.tsx +++ b/src/react/hoc/subscription-hoc.tsx @@ -15,6 +15,11 @@ import { } from "./hoc-utils.js"; import type { OperationOption, OptionProps, DataProps } from "./types.js"; +/** + * @deprecated + * Official support for React Apollo higher order components ended in March 2020. + * This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. + */ export function withSubscription< TProps extends TGraphQLVariables | {} = {}, TData extends object = {}, diff --git a/src/react/hoc/withApollo.tsx b/src/react/hoc/withApollo.tsx index 3b73c1f692d..54cb7c67be9 100644 --- a/src/react/hoc/withApollo.tsx +++ b/src/react/hoc/withApollo.tsx @@ -10,6 +10,11 @@ function getDisplayName

(WrappedComponent: ReactTypes.ComponentType

) { return WrappedComponent.displayName || WrappedComponent.name || "Component"; } +/** + * @deprecated + * Official support for React Apollo higher order components ended in March 2020. + * This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. + */ export function withApollo( WrappedComponent: ReactTypes.ComponentType< WithApolloClient> From de5b878ebfbfee956f3396e1f4e38a42fa3df017 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 20 Dec 2023 21:39:07 +0100 Subject: [PATCH 73/90] Add a "memory management" documentation page (#11415) Co-authored-by: Maria Elisabeth Schreiber Co-authored-by: Jerel Miller Co-authored-by: jerelmiller --- .api-reports/api-report-core.md | 2 - .api-reports/api-report-react.md | 2 - .api-reports/api-report-react_components.md | 2 - .api-reports/api-report-react_context.md | 2 - .api-reports/api-report-react_hoc.md | 2 - .api-reports/api-report-react_hooks.md | 2 - .api-reports/api-report-react_internal.md | 2 - .api-reports/api-report-react_ssr.md | 2 - .api-reports/api-report-testing.md | 2 - .api-reports/api-report-testing_core.md | 2 - .api-reports/api-report-utilities.md | 2 - .api-reports/api-report.md | 2 - config/apiExtractor.ts | 106 +++++++----- docs/shared/ApiDoc/DocBlock.js | 2 +- docs/shared/ApiDoc/PropertySignatureTable.js | 7 +- docs/source/caching/memory-management.mdx | 126 ++++++++++++++ docs/source/config.json | 1 + src/core/ApolloClient.ts | 75 ++++++++- src/utilities/caching/sizes.ts | 163 +++++++++++++------ 19 files changed, 386 insertions(+), 118 deletions(-) create mode 100644 docs/source/caching/memory-management.mdx diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 04da92c9c0a..54622181969 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -109,8 +109,6 @@ export class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index c6de1672ff0..14db26ad941 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -120,8 +120,6 @@ class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; diff --git a/.api-reports/api-report-react_components.md b/.api-reports/api-report-react_components.md index 0775e0c9cbb..fed0f1ea60b 100644 --- a/.api-reports/api-report-react_components.md +++ b/.api-reports/api-report-react_components.md @@ -120,8 +120,6 @@ class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; diff --git a/.api-reports/api-report-react_context.md b/.api-reports/api-report-react_context.md index c93ef634c07..01f8cfa71d1 100644 --- a/.api-reports/api-report-react_context.md +++ b/.api-reports/api-report-react_context.md @@ -119,8 +119,6 @@ class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; diff --git a/.api-reports/api-report-react_hoc.md b/.api-reports/api-report-react_hoc.md index ca318274946..044cb10cc77 100644 --- a/.api-reports/api-report-react_hoc.md +++ b/.api-reports/api-report-react_hoc.md @@ -119,8 +119,6 @@ class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index f6d137505ce..3296d232fd0 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -118,8 +118,6 @@ class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; diff --git a/.api-reports/api-report-react_internal.md b/.api-reports/api-report-react_internal.md index 6eab8934062..a54ec58ee63 100644 --- a/.api-reports/api-report-react_internal.md +++ b/.api-reports/api-report-react_internal.md @@ -118,8 +118,6 @@ class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; diff --git a/.api-reports/api-report-react_ssr.md b/.api-reports/api-report-react_ssr.md index 060eb9c79e6..8f22892c265 100644 --- a/.api-reports/api-report-react_ssr.md +++ b/.api-reports/api-report-react_ssr.md @@ -119,8 +119,6 @@ class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index 57c3dc01694..54327b058c4 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -119,8 +119,6 @@ class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 341da7c1d35..1b505f4d121 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -118,8 +118,6 @@ class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 1478b419cec..8028dbdf360 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -131,8 +131,6 @@ class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 9057e4f8f2a..627586c9bd9 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -111,8 +111,6 @@ export class ApolloClient implements DataProxy { get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts - // - // @internal getMemoryInternals?: typeof getApolloClientMemoryInternals; getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; diff --git a/config/apiExtractor.ts b/config/apiExtractor.ts index f50d25a875a..f64b0d7b525 100644 --- a/config/apiExtractor.ts +++ b/config/apiExtractor.ts @@ -6,6 +6,7 @@ import { IConfigFile, } from "@microsoft/api-extractor"; import { parseArgs } from "node:util"; +import fs from "node:fs"; // @ts-ignore import { map } from "./entryPoints.js"; @@ -40,37 +41,67 @@ const packageJsonFullPath = path.resolve(__dirname, "../package.json"); process.exitCode = 0; -map((entryPoint: { dirs: string[] }) => { - if (entryPoint.dirs.length > 0 && parsed.values["main-only"]) return; +const tempDir = fs.mkdtempSync("api-model"); +try { + if (parsed.values.generate?.includes("docModel")) { + console.log( + "\n\nCreating API extractor docmodel for the a combination of all entry points" + ); + const dist = path.resolve(__dirname, "../dist"); + const entryPoints = map((entryPoint: { dirs: string[] }) => { + return `export * from "${dist}/${entryPoint.dirs.join("/")}/index.d.ts";`; + }).join("\n"); + const entryPointFile = path.join(tempDir, "entry.d.ts"); + fs.writeFileSync(entryPointFile, entryPoints); + + buildReport(entryPointFile, "docModel"); + } - const path = entryPoint.dirs.join("/"); - const mainEntryPointFilePath = - `/dist/${path}/index.d.ts`.replace("//", "/"); - console.log( - "\n\nCreating API extractor report for " + mainEntryPointFilePath - ); + if (parsed.values.generate?.includes("apiReport")) { + map((entryPoint: { dirs: string[] }) => { + const path = entryPoint.dirs.join("/"); + const mainEntryPointFilePath = + `/dist/${path}/index.d.ts`.replace("//", "/"); + console.log( + "\n\nCreating API extractor report for " + mainEntryPointFilePath + ); + buildReport( + mainEntryPointFilePath, + "apiReport", + `api-report${path ? "-" + path.replace(/\//g, "_") : ""}.md` + ); + }); + } +} finally { + fs.rmSync(tempDir, { recursive: true }); +} +function buildReport( + mainEntryPointFilePath: string, + mode: "apiReport" | "docModel", + reportFileName = "" +) { const configObject: IConfigFile = { ...(JSON.parse(JSON.stringify(baseConfig)) as IConfigFile), mainEntryPointFilePath, }; - configObject.apiReport!.reportFileName = `api-report${ - path ? "-" + path.replace(/\//g, "_") : "" - }.md`; - - configObject.apiReport!.enabled = - parsed.values.generate?.includes("apiReport") || false; - - configObject.docModel!.enabled = - parsed.values.generate?.includes("docModel") || false; - - if (entryPoint.dirs.length !== 0) { + if (mode === "apiReport") { + configObject.apiReport!.enabled = true; configObject.docModel = { enabled: false }; - configObject.tsdocMetadata = { enabled: false }; configObject.messages!.extractorMessageReporting![ "ae-unresolved-link" ]!.logLevel = ExtractorLogLevel.None; + configObject.apiReport!.reportFileName = reportFileName; + } else { + configObject.docModel!.enabled = true; + configObject.apiReport = { + enabled: false, + // this has to point to an existing folder, otherwise the extractor will fail + // but it will not write the file + reportFileName: "disabled.md", + reportFolder: tempDir, + }; } const extractorConfig = ExtractorConfig.prepare({ @@ -85,22 +116,23 @@ map((entryPoint: { dirs: string[] }) => { }); let succeededAdditionalChecks = true; - const contents = readFileSync(extractorConfig.reportFilePath, "utf8"); - - if (contents.includes("rehackt")) { - succeededAdditionalChecks = false; - console.error( - "❗ %s contains a reference to the `rehackt` package!", - extractorConfig.reportFilePath - ); - } - if (contents.includes('/// ')) { - succeededAdditionalChecks = false; - console.error( - "❗ %s contains a reference to the global `React` type!/n" + - 'Use `import type * as ReactTypes from "react";` instead', - extractorConfig.reportFilePath - ); + if (fs.existsSync(extractorConfig.reportFilePath)) { + const contents = readFileSync(extractorConfig.reportFilePath, "utf8"); + if (contents.includes("rehackt")) { + succeededAdditionalChecks = false; + console.error( + "❗ %s contains a reference to the `rehackt` package!", + extractorConfig.reportFilePath + ); + } + if (contents.includes('/// ')) { + succeededAdditionalChecks = false; + console.error( + "❗ %s contains a reference to the global `React` type!/n" + + 'Use `import type * as ReactTypes from "react";` instead', + extractorConfig.reportFilePath + ); + } } if (extractorResult.succeeded && succeededAdditionalChecks) { @@ -115,4 +147,4 @@ map((entryPoint: { dirs: string[] }) => { } process.exitCode = 1; } -}); +} diff --git a/docs/shared/ApiDoc/DocBlock.js b/docs/shared/ApiDoc/DocBlock.js index 333bda75afd..157ece36fdc 100644 --- a/docs/shared/ApiDoc/DocBlock.js +++ b/docs/shared/ApiDoc/DocBlock.js @@ -138,7 +138,7 @@ export function Example({ if (!value) return null; return ( - {mdToReact(value)} + {mdToReact(value)} ); } diff --git a/docs/shared/ApiDoc/PropertySignatureTable.js b/docs/shared/ApiDoc/PropertySignatureTable.js index 317b2926f0f..b5d31feb18d 100644 --- a/docs/shared/ApiDoc/PropertySignatureTable.js +++ b/docs/shared/ApiDoc/PropertySignatureTable.js @@ -98,7 +98,12 @@ export function PropertySignatureTable({ - + ))} diff --git a/docs/source/caching/memory-management.mdx b/docs/source/caching/memory-management.mdx new file mode 100644 index 00000000000..cf40fc27d8d --- /dev/null +++ b/docs/source/caching/memory-management.mdx @@ -0,0 +1,126 @@ +--- +title: Memory management +api_doc: + - "@apollo/client!CacheSizes:interface" + - "@apollo/client!ApolloClient:class" +subtitle: Learn how to choose and set custom cache sizes +description: Learn how to choose and set custom cache sizes with Apollo Client. +minVersion: 3.9.0 +--- + +import { Remarks, PropertySignatureTable, Example } from '../../shared/ApiDoc'; + +## Cache Sizes + +For better performance, Apollo Client caches (or, in other words, memoizes) many +internally calculated values. +In most cases, these values are cached in [weak caches](https://en.wikipedia.org/wiki/Weak_reference), which means that if the +source object is garbage-collected, the cached value will be garbage-collected, +too. + +These caches are also Least Recently Used (LRU) caches, meaning that if the cache is full, +the least recently used value will be garbage-collected. + +Depending on your application, you might want to tweak the cache size to fit your +needs. + +You can set your cache size [before (recommended)](#setting-cache-sizes-before-loading-the-apollo-client-library) or [after](#adjusting-cache-sizes-after-loading-the-apollo-client-library) loading the Apollo Client library. + +### Setting cache sizes before loading the Apollo Client library + +Setting cache sizes before loading the Apollo Client library is recommended because some caches are already initialized when the library is loaded. Changed cache sizes only +affect caches created after the fact, so you'd have to write additional runtime code to recreate these caches after changing their size. + + ```ts +import type { CacheSizes } from '@apollo/client/utilities'; + + globalThis[Symbol.for("apollo.cacheSize")] = { + parser: 100, + "fragmentRegistry.lookup": 500 + } satisfies Partial + ``` + +### Adjusting cache sizes after loading the Apollo Client library + +You can also adjust cache sizes after loading the library. + +```js +import { cacheSizes } from '@apollo/client/utilities'; +import { print } from '@apollo/client' + +cacheSizes.print = 100; +// cache sizes changed this way will only take effect for caches +// created after the cache size has been changed, so we need to +// reset the cache for it to be effective + +print.reset(); +``` + +### Choosing appropriate cache sizes + + + +To choose good sizes for our memoization caches, you need to know what they +use as source values, and have a general understanding of the data flow inside of +Apollo Client. + +For most memoized values, the source value is a parsed GraphQL document— +a `DocumentNode`. There are two types: + +* **User-supplied `DocumentNode`s** are created + by the user, for example by using the `gql` template literal tag. + This is the `QUERY`, `MUTATION`, or `SUBSCRIPTION` argument passed + into a [`useQuery` hook](../data/queries/#usequery-api) or as the `query` option to `client.query`. +* **Transformed `DocumentNode`s** are derived from + user-supplied `DocumentNode`s, for example, by applying [`DocumentTransform`s](../data/document-transforms/) to them. + +As a rule of thumb, you should set the cache sizes for caches using a transformed +`DocumentNode` at least to the same size as for caches using a user-supplied +`DocumentNode`. If your application uses a custom `DocumentTransform` that does +not always transform the same input to the same output, you should set the cache +size for caches using a Transformed `DocumentNode` to a higher value than for +caches using a user-supplied `DocumentNode`. + +By default, Apollo Client uses a base value of 1000 cached objects for caches using +user-supplied `DocumentNode` instances, and scales other cache sizes relative +to that. For example, the default base value of 1000 for user-provided `DocumentNode`s would scale to 2000, 4000, etc. for transformed `DocumentNode`s, depending on the transformation performed. + +This base value should be plenty for most applications, but you can tweak them if you have different requirements. + +#### Measuring cache usage + +Since estimating appropriate cache sizes for your application can be hard, Apollo Client +exposes an API for cache usage measurement.
+This way, you can click around in your application and then take a look at the +actual usage of the memoizing caches. + +Keep in mind that this API is primarily meant for usage with the Apollo DevTools +(an integration is coming soon), and the API may change at any +point in time.
+It is also only included in development builds, not in production builds. + + + +The cache usage API is only meant for manual measurements. Don't rely on it in production code or tests. + + + + + + + +### Cache options + + diff --git a/docs/source/config.json b/docs/source/config.json index 6862f2e7836..60b6f0b1b6e 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -28,6 +28,7 @@ "Reading and writing": "/caching/cache-interaction", "Garbage collection and eviction": "/caching/garbage-collection", "Customizing field behavior": "/caching/cache-field-behavior", + "Memory Management": "/caching/memory-management", "Advanced topics": "/caching/advanced-topics" }, "Pagination": { diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index a3216fdda34..933d4266b1e 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -750,10 +750,83 @@ export class ApolloClient implements DataProxy { /** * @experimental - * @internal * This is not a stable API - it is used in development builds to expose * information to the DevTools. * Use at your own risk! + * For more details, see [Memory Management](https://www.apollographql.com/docs/react/caching/memory-management/#measuring-cache-usage) + * + * @example + * ```ts + * console.log(client.getMemoryInternals()) + * ``` + * Logs output in the following JSON format: + * @example + * ```json + *{ + * limits: { + * parser: 1000, + * canonicalStringify: 1000, + * print: 2000, + * 'documentTransform.cache': 2000, + * 'queryManager.getDocumentInfo': 2000, + * 'PersistedQueryLink.persistedQueryHashes': 2000, + * 'fragmentRegistry.transform': 2000, + * 'fragmentRegistry.lookup': 1000, + * 'fragmentRegistry.findFragmentSpreads': 4000, + * 'cache.fragmentQueryDocuments': 1000, + * 'removeTypenameFromVariables.getVariableDefinitions': 2000, + * 'inMemoryCache.maybeBroadcastWatch': 5000, + * 'inMemoryCache.executeSelectionSet': 10000, + * 'inMemoryCache.executeSubSelectedArray': 5000 + * }, + * sizes: { + * parser: 26, + * canonicalStringify: 4, + * print: 14, + * addTypenameDocumentTransform: [ + * { + * cache: 14, + * }, + * ], + * queryManager: { + * getDocumentInfo: 14, + * documentTransforms: [ + * { + * cache: 14, + * }, + * { + * cache: 14, + * }, + * ], + * }, + * fragmentRegistry: { + * findFragmentSpreads: 34, + * lookup: 20, + * transform: 14, + * }, + * cache: { + * fragmentQueryDocuments: 22, + * }, + * inMemoryCache: { + * executeSelectionSet: 4345, + * executeSubSelectedArray: 1206, + * maybeBroadcastWatch: 32, + * }, + * links: [ + * { + * PersistedQueryLink: { + * persistedQueryHashes: 14, + * }, + * }, + * { + * removeTypenameFromVariables: { + * getVariableDefinitions: 14, + * }, + * }, + * ], + * }, + * } + *``` */ public getMemoryInternals?: typeof getApolloClientMemoryInternals; } diff --git a/src/utilities/caching/sizes.ts b/src/utilities/caching/sizes.ts index 998537740a3..28ace70a8c2 100644 --- a/src/utilities/caching/sizes.ts +++ b/src/utilities/caching/sizes.ts @@ -9,97 +9,133 @@ declare global { /** * The cache sizes used by various Apollo Client caches. * - * Note that these caches are all derivative and if an item is cache-collected, - * it's not the end of the world - the cached item will just be recalculated. - * - * As a result, these cache sizes should not be chosen to hold every value ever - * encountered, but rather to hold a reasonable number of values that can be - * assumed to be on the screen at any given time. + * @remarks + * All configurable caches hold memoized values. If an item is + * cache-collected, it incurs only a small performance impact and + * doesn't cause data loss. A smaller cache size might save you memory. * + * You should choose cache sizes appropriate for storing a reasonable + * number of values rather than every value. To prevent too much recalculation, + * choose cache sizes that are at least large enough to hold memoized values for + * all hooks/queries on the screen at any given time. + */ +/* * We assume a "base value" of 1000 here, which is already very generous. * In most applications, it will be very unlikely that 1000 different queries * are on screen at the same time. */ export interface CacheSizes { /** - * Cache size for the [`print`](../../utilities/graphql/print.ts) function. + * Cache size for the [`print`](https://github.com/apollographql/apollo-client/blob/main/src/utilities/graphql/print.ts) function. + * + * It is called with transformed `DocumentNode`s. * * @defaultValue * Defaults to `2000`. * * @remarks - * This method is called from the `QueryManager` and various `Link`s, + * This method is called to transform a GraphQL query AST parsed by `gql` + * back into a GraphQL string. + * + * @privateRemarks + * This method is called from the `QueryManager` and various `ApolloLink`s, * always with the "serverQuery", so the server-facing part of a transformed - * DocumentNode. + * `DocumentNode`. */ print: number; /** - * Cache size for the [`parser`](../../react/parser/index.ts) function. + * Cache size for the [`parser`](https://github.com/apollographql/apollo-client/blob/main/src/react/parser/index.ts) function. + * + * It is called with user-provided `DocumentNode`s. * * @defaultValue * Defaults to `1000`. * * @remarks + * This method is called by HOCs and hooks. + * + * @privateRemarks * This function is used directly in HOCs, and nowadays mainly accessed by * calling `verifyDocumentType` from various hooks. * It is called with a user-provided DocumentNode. */ parser: number; /** - * Cache size for the `performWork` method of each [`DocumentTransform`](../../utilities/graphql/DocumentTransform.ts). + * Cache size for the cache of [`DocumentTransform`](https://github.com/apollographql/apollo-client/blob/main/src/utilities/graphql/DocumentTransform.ts) + * instances with the `cache` option set to `true`. + * + * Can be called with user-defined or already-transformed `DocumentNode`s. * * @defaultValue * Defaults to `2000`. * * @remarks - * This method is called from `transformDocument`, which is called from - * `QueryManager` with a user-provided DocumentNode. - * It is also called with already-transformed DocumentNodes, assuming the - * user provided additional transforms. + * The cache size here should be chosen with other `DocumentTransform`s in mind. + * For example, if there was a `DocumentTransform` that would take `x` `DocumentNode`s, + * and returned a differently-transformed `DocumentNode` depending if the app is + * online or offline, then we assume that the cache returns `2*x` documents. + * If that were concatenated with another `DocumentTransform` that would + * also duplicate the cache size, you'd need to account for `4*x` documents + * returned by the second transform. + * + * Due to an implementation detail of Apollo Client, if you use custom document + * transforms you should always add `n` (the "base" number of user-provided + * Documents) to the resulting cache size. * - * The cache size here should be chosen with other DocumentTransforms in mind. - * For example, if there was a DocumentTransform that would take `n` DocumentNodes, - * and returned a differently-transformed DocumentNode depending if the app is - * online or offline, then we assume that the cache returns `2*n` documents. + * If we assume that the user-provided transforms receive `n` documents and + * return `n` documents, the cache size should be `2*n`. + * + * If we assume that the chain of user-provided transforms receive `n` documents and + * return `4*n` documents, the cache size should be `5*n`. + * + * This size should also then be used in every other cache that mentions that + * it operates on a "transformed" `DocumentNode`. + * + * @privateRemarks + * Cache size for the `performWork` method of each [`DocumentTransform`](https://github.com/apollographql/apollo-client/blob/main/src/utilities/graphql/DocumentTransform.ts). * * No user-provided DocumentNode will actually be "the last one", as we run the * `defaultDocumentTransform` before *and* after the user-provided transforms. + * For that reason, we need the extra `n` here - `n` for "before transformation" + * plus the actual maximum cache size of the user-provided transform chain. * - * So if we assume that the user-provided transforms receive `n` documents and - * return `n` documents, the cache size should be `2*n`. - * - * If we assume that the user-provided transforms receive `n` documents and - * returns `2*n` documents, the cache size should be `3*n`. + * This method is called from `transformDocument`, which is called from + * `QueryManager` with a user-provided DocumentNode. + * It is also called with already-transformed DocumentNodes, assuming the + * user provided additional transforms. * - * This size should also then be used in every other cache that mentions that - * it operates on a "transformed" DocumentNode. */ "documentTransform.cache": number; /** - * Cache size for the `transformCache` used in the `getDocumentInfo` method of - * [`QueryManager`](../../core/QueryManager.ts). + * A cache inside of [`QueryManager`](https://github.com/apollographql/apollo-client/blob/main/src/core/QueryManager.ts). + * + * It is called with transformed `DocumentNode`s. * * @defaultValue * Defaults to `2000`. * - * @remarks - * `getDocumentInfo` is called throughout the `QueryManager` with transformed - * DocumentNodes. + * @privateRemarks + * Cache size for the `transformCache` used in the `getDocumentInfo` method of `QueryManager`. + * Called throughout the `QueryManager` with transformed DocumentNodes. */ "queryManager.getDocumentInfo": number; /** - * Cache size for the `hashesByQuery` cache in the [`PersistedQueryLink`](../../link/persisted-queries/index.ts). + * A cache inside of [`PersistedQueryLink`](https://github.com/apollographql/apollo-client/blob/main/src/link/persisted-queries/index.ts). + * + * It is called with transformed `DocumentNode`s. * * @defaultValue * Defaults to `2000`. * * @remarks - * This cache is used to cache the hashes of persisted queries. It is working with - * transformed DocumentNodes. + * This cache is used to cache the hashes of persisted queries. + * + * @privateRemarks + * Cache size for the `hashesByQuery` cache in the `PersistedQueryLink`. */ "PersistedQueryLink.persistedQueryHashes": number; /** - * Cache for the `sortingMap` used by [`canonicalStringify`](../../utilities/common/canonicalStringify.ts). + * Cache used by [`canonicalStringify`](https://github.com/apollographql/apollo-client/blob/main/src/utilities/common/canonicalStringify.ts). * * @defaultValue * Defaults to `1000`. @@ -110,52 +146,67 @@ export interface CacheSizes { * It uses the stringified unsorted keys of objects as keys. * The cache will not grow beyond the size of different object **shapes** * encountered in an application, no matter how much actual data gets stringified. + * + * @privateRemarks + * Cache size for the `sortingMap` in `canonicalStringify`. */ canonicalStringify: number; /** - * Cache size for the `transform` method of [`FragmentRegistry`](../../cache/inmemory/fragmentRegistry.ts). + * A cache inside of [`FragmentRegistry`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/fragmentRegistry.ts). + * + * Can be called with user-defined or already-transformed `DocumentNode`s. * * @defaultValue * Defaults to `2000`. * - * @remarks + * @privateRemarks + * + * Cache size for the `transform` method of FragmentRegistry. * This function is called as part of the `defaultDocumentTransform` which will be called with * user-provided and already-transformed DocumentNodes. * */ "fragmentRegistry.transform": number; /** - * Cache size for the `lookup` method of [`FragmentRegistry`](../../cache/inmemory/fragmentRegistry.ts). + * A cache inside of [`FragmentRegistry`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/fragmentRegistry.ts). + * + * This function is called with fragment names in the form of a string. * * @defaultValue * Defaults to `1000`. * * @remarks - * This function is called with fragment names in the form of a string. + * The size of this case should be chosen with the number of fragments in + * your application in mind. * * Note: - * This function is a dependency of `transform`, so having a too small cache size here + * This function is a dependency of `fragmentRegistry.transform`, so having too small of a cache size here * might involuntarily invalidate values in the `transform` cache. + * + * @privateRemarks + * Cache size for the `lookup` method of FragmentRegistry. */ "fragmentRegistry.lookup": number; /** - * Cache size for the `findFragmentSpreads` method of [`FragmentRegistry`](../../cache/inmemory/fragmentRegistry.ts). + * Cache size for the `findFragmentSpreads` method of [`FragmentRegistry`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/fragmentRegistry.ts). + * + * This function is called with transformed `DocumentNode`s, as well as recursively + * with every fragment spread referenced within that, or a fragment referenced by a + * fragment spread. * * @defaultValue * Defaults to `4000`. * * @remarks - * This function is called with transformed DocumentNodes, as well as recursively - * with every fragment spread referenced within that, or a fragment referenced by a - * fragment spread. * - * Note: - * This function is a dependency of `transform`, so having a too small cache size here + * Note: This function is a dependency of `fragmentRegistry.transform`, so having too small of cache size here * might involuntarily invalidate values in the `transform` cache. */ "fragmentRegistry.findFragmentSpreads": number; /** - * Cache size for the `getFragmentDoc` method of [`ApolloCache`](../../cache/core/cache.ts). + * Cache size for the `getFragmentDoc` method of [`ApolloCache`](https://github.com/apollographql/apollo-client/blob/main/src/cache/core/cache.ts). + * + * This function is called with user-provided fragment definitions. * * @defaultValue * Defaults to `1000`. @@ -165,19 +216,21 @@ export interface CacheSizes { */ "cache.fragmentQueryDocuments": number; /** - * Cache size for the `getVariableDefinitions` function in [`removeTypenameFromVariables`](../../link/remove-typename/removeTypenameFromVariables.ts). + * Cache used in [`removeTypenameFromVariables`](https://github.com/apollographql/apollo-client/blob/main/src/link/remove-typename/removeTypenameFromVariables.ts). + * + * This function is called transformed `DocumentNode`s. * * @defaultValue * Defaults to `2000`. * - * @remarks - * This function is called in a link with transformed DocumentNodes. + * @privateRemarks + * Cache size for the `getVariableDefinitions` function of `removeTypenameFromVariables`. */ "removeTypenameFromVariables.getVariableDefinitions": number; /** - * Cache size for the `maybeBroadcastWatch` method on [`InMemoryCache`](../../cache/inmemory/inMemoryCache.ts). + * Cache size for the `maybeBroadcastWatch` method on [`InMemoryCache`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/inMemoryCache.ts). * - * `maybeBroadcastWatch` will be set to the `resultCacheMaxSize` option and + * Note: `maybeBroadcastWatch` will be set to the `resultCacheMaxSize` option and * will fall back to this configuration value if the option is not set. * * @defaultValue @@ -192,8 +245,9 @@ export interface CacheSizes { */ "inMemoryCache.maybeBroadcastWatch": number; /** - * Cache size for the `executeSelectionSet` method on [`StoreReader`](../../cache/inmemory/readFromStore.ts). + * Cache size for the `executeSelectionSet` method on [`StoreReader`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/readFromStore.ts). * + * Note: * `executeSelectionSet` will be set to the `resultCacheMaxSize` option and * will fall back to this configuration value if the option is not set. * @@ -206,8 +260,9 @@ export interface CacheSizes { */ "inMemoryCache.executeSelectionSet": number; /** - * Cache size for the `executeSubSelectedArray` method on [`StoreReader`](../../cache/inmemory/readFromStore.ts). + * Cache size for the `executeSubSelectedArray` method on [`StoreReader`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/readFromStore.ts). * + * Note: * `executeSubSelectedArray` will be set to the `resultCacheMaxSize` option and * will fall back to this configuration value if the option is not set. * From 4b6f2bccf3ba94643b38689b32edd2839e47aec1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 21 Dec 2023 04:12:15 -0700 Subject: [PATCH 74/90] Remove `retain` from `useLoadableQuery` to allow for auto disposal (#11442) * Don't retain in useLoadableQuery and allow it to auto dispose * Add test to show it auto disposes in configured timeout * Add test to ensure useLoadableQuery can remount and work as expected * Add test to ensure auto subscribe after natural dispose works * Add changeset * Update size limits * trigger ci * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: Lenz Weber-Tronic Co-authored-by: phryneas --- .changeset/late-rabbits-protect.md | 7 + .size-limits.json | 2 +- .../hooks/__tests__/useLoadableQuery.test.tsx | 262 ++++++++++++++++++ src/react/hooks/useLoadableQuery.ts | 2 - 4 files changed, 270 insertions(+), 3 deletions(-) create mode 100644 .changeset/late-rabbits-protect.md diff --git a/.changeset/late-rabbits-protect.md b/.changeset/late-rabbits-protect.md new file mode 100644 index 00000000000..1494b569018 --- /dev/null +++ b/.changeset/late-rabbits-protect.md @@ -0,0 +1,7 @@ +--- +'@apollo/client': minor +--- + +Remove the need to call `retain` from `useLoadableQuery` since `useReadQuery` will now retain the query. This means that a `queryRef` that is not consumed by `useReadQuery` within the given `autoDisposeTimeoutMs` will now be auto diposed for you. + +Thanks to [#11412](https://github.com/apollographql/apollo-client/pull/11412), disposed query refs will be automatically resubscribed to the query when consumed by `useReadQuery` after it has been disposed. diff --git a/.size-limits.json b/.size-limits.json index f78538c39a5..d284dce0aa7 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39136, + "dist/apollo-client.min.cjs": 39129, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32663 } diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 62f0827c7bf..68ef6a7e9a7 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -47,11 +47,17 @@ import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import invariant, { InvariantError } from "ts-invariant"; import { Profiler, + SimpleCaseData, createProfiler, + setupSimpleCase, spyOnConsole, useTrackRenders, } from "../../../testing/internal"; +afterEach(() => { + jest.useRealTimers(); +}); + interface SimpleQueryData { greeting: string; } @@ -404,6 +410,262 @@ it("tears down the query on unmount", async () => { expect(client).not.toHaveSuspenseCacheEntryUsing(query); }); +it("auto disposes of the queryRef if not used within timeout", async () => { + jest.useFakeTimers(); + const { query } = setupSimpleCase(); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ link, cache: new InMemoryCache() }); + + const { result } = renderHook(() => useLoadableQuery(query, { client })); + const [loadQuery] = result.current; + + act(() => loadQuery()); + const [, queryRef] = result.current; + + expect(queryRef!).not.toBeDisposed(); + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + await act(async () => { + link.simulateResult({ result: { data: { greeting: "Hello" } } }, true); + // Ensure simulateResult will deliver the result since its wrapped with + // setTimeout + await jest.advanceTimersByTimeAsync(10); + }); + + jest.advanceTimersByTime(30_000); + + expect(queryRef!).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +it("auto disposes of the queryRef if not used within configured timeout", async () => { + jest.useFakeTimers(); + const { query } = setupSimpleCase(); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + defaultOptions: { + react: { + suspense: { + autoDisposeTimeoutMs: 5000, + }, + }, + }, + }); + + const { result } = renderHook(() => useLoadableQuery(query, { client })); + const [loadQuery] = result.current; + + act(() => loadQuery()); + const [, queryRef] = result.current; + + expect(queryRef!).not.toBeDisposed(); + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + await act(async () => { + link.simulateResult({ result: { data: { greeting: "Hello" } } }, true); + // Ensure simulateResult will deliver the result since its wrapped with + // setTimeout + await jest.advanceTimersByTimeAsync(10); + }); + + jest.advanceTimersByTime(5000); + + expect(queryRef!).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +it("will resubscribe after disposed when mounting useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + defaultOptions: { + react: { + suspense: { + // Set this to something really low to avoid fake timers + autoDisposeTimeoutMs: 20, + }, + }, + }, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(false); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + + }> + {show && queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } + + // Wait long enough for auto dispose to kick in + await wait(50); + + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + await act(() => user.click(screen.getByText("Toggle"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ + query, + data: { greeting: "Hello again" }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it("auto resubscribes when mounting useReadQuery after naturally disposed by useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + + }> + {show && queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + await act(() => user.click(toggleButton)); + + expect(client.getObservableQueries().size).toBe(1); + // Here we don't expect a suspense cache entry because we previously disposed + // of it and did not call `loadQuery` again, which would normally add it to + // the suspense cache + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ + query, + data: { greeting: "Hello again" }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + it("changes variables on a query and resuspends when passing new variables to the loadQuery function", async () => { const { query, mocks } = useVariablesQueryCase(); diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 3b4d9fba348..796721a92fc 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -132,8 +132,6 @@ export function useLoadableQuery< const calledDuringRender = useRenderGuard(); - React.useEffect(() => internalQueryRef?.retain(), [internalQueryRef]); - const fetchMore: FetchMoreFunction = React.useCallback( (options) => { if (!internalQueryRef) { From fe56f681c06cf35715b4d8d49cc127adb74782a2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:47:22 +0100 Subject: [PATCH 75/90] Version Packages (beta) (#11440) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 6 ++++++ CHANGELOG.md | 33 +++++++++++++++++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 4f54090a3c4..4e9f230f135 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -7,13 +7,16 @@ "changesets": [ "beige-geese-wink", "breezy-spiders-tap", + "chatty-comics-yawn", "clean-items-smash", "cold-llamas-turn", + "curvy-seas-hope", "dirty-kids-crash", "dirty-tigers-matter", "forty-cups-shop", "friendly-clouds-laugh", "hot-ducks-burn", + "late-rabbits-protect", "mighty-coats-check", "polite-avocados-warn", "quick-hats-marry", @@ -21,7 +24,9 @@ "shaggy-ears-scream", "shaggy-sheep-pull", "sixty-boxes-rest", + "smooth-plums-shout", "sour-sheep-walk", + "spicy-drinks-camp", "strong-terms-perform", "swift-zoos-collect", "thick-mice-collect", @@ -32,6 +37,7 @@ "violet-lions-draw", "wet-forks-rhyme", "wild-dolphins-jog", + "wise-news-grab", "yellow-flies-repeat" ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 535667bf6e3..f165a8c1a78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,38 @@ # @apollo/client +## 3.9.0-beta.1 + +### Minor Changes + +- [#11424](https://github.com/apollographql/apollo-client/pull/11424) [`62f3b6d`](https://github.com/apollographql/apollo-client/commit/62f3b6d0e89611e27d9f29812ee60e5db5963fd6) Thanks [@phryneas](https://github.com/phryneas)! - Simplify RetryLink, fix potential memory leak + + Historically, `RetryLink` would keep a `values` array of all previous values, + in case the operation would get an additional subscriber at a later point in time. + In practice, this could lead to a memory leak (#11393) and did not serve any + further purpose, as the resulting observable would only be subscribed to by + Apollo Client itself, and only once - it would be wrapped in a `Concast` before + being exposed to the user, and that `Concast` would handle subscribers on its + own. + +- [#11442](https://github.com/apollographql/apollo-client/pull/11442) [`4b6f2bc`](https://github.com/apollographql/apollo-client/commit/4b6f2bccf3ba94643b38689b32edd2839e47aec1) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Remove the need to call `retain` from `useLoadableQuery` since `useReadQuery` will now retain the query. This means that a `queryRef` that is not consumed by `useReadQuery` within the given `autoDisposeTimeoutMs` will now be auto diposed for you. + + Thanks to [#11412](https://github.com/apollographql/apollo-client/pull/11412), disposed query refs will be automatically resubscribed to the query when consumed by `useReadQuery` after it has been disposed. + +- [#11438](https://github.com/apollographql/apollo-client/pull/11438) [`6d46ab9`](https://github.com/apollographql/apollo-client/commit/6d46ab930a5e9bd5cae153d3b75b8966784fcd4e) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Remove the need to call `retain` from `useBackgroundQuery` since `useReadQuery` will now retain the query. This means that a `queryRef` that is not consumed by `useReadQuery` within the given `autoDisposeTimeoutMs` will now be auto diposed for you. + + Thanks to [#11412](https://github.com/apollographql/apollo-client/pull/11412), disposed query refs will be automatically resubscribed to the query when consumed by `useReadQuery` after it has been disposed. + +### Patch Changes + +- [#11443](https://github.com/apollographql/apollo-client/pull/11443) [`ff5a332`](https://github.com/apollographql/apollo-client/commit/ff5a332ff8b190c418df25371e36719d70061ebe) Thanks [@phryneas](https://github.com/phryneas)! - Adds a deprecation warning to the HOC and render prop APIs. + + The HOC and render prop APIs have already been deprecated since 2020, + but we previously didn't have a @deprecated tag in the DocBlocks. + +- [#11078](https://github.com/apollographql/apollo-client/pull/11078) [`14edebe`](https://github.com/apollographql/apollo-client/commit/14edebebefb7634c32b921d02c1c85c6c8737989) Thanks [@phryneas](https://github.com/phryneas)! - ObservableQuery: prevent reporting results of previous queries if the variables changed since + +- [#11439](https://github.com/apollographql/apollo-client/pull/11439) [`33454f0`](https://github.com/apollographql/apollo-client/commit/33454f0a40a05ea2b00633bda20a84d0ec3a4f4d) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Address bundling issue introduced in [#11412](https://github.com/apollographql/apollo-client/pull/11412) where the `react/cache` internals ended up duplicated in the bundle. This was due to the fact that we had a `react/hooks` entrypoint that imported these files along with the newly introduced `createQueryPreloader` function, which lived outside of the `react/hooks` folder. + ## 3.9.0-beta.0 ### Minor Changes diff --git a/package-lock.json b/package-lock.json index 8c05475ae72..1e456f9219b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.0-beta.0", + "version": "3.9.0-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.0-beta.0", + "version": "3.9.0-beta.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 2d43aee4708..26d134f3603 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.0-beta.0", + "version": "3.9.0-beta.1", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From d75811860de52f91334ea2a1e62732cb6aa3b578 Mon Sep 17 00:00:00 2001 From: Alessia Bellisario Date: Wed, 3 Jan 2024 14:36:21 -0500 Subject: [PATCH 76/90] chore: add docs for relay and urql network adapters (#11460) --- docs/source/data/subscriptions.mdx | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/source/data/subscriptions.mdx b/docs/source/data/subscriptions.mdx index c4c177c9784..1c48ac253ca 100644 --- a/docs/source/data/subscriptions.mdx +++ b/docs/source/data/subscriptions.mdx @@ -49,6 +49,51 @@ To use Apollo Client with a GraphQL endpoint that supports [multipart subscripti Aside from updating your client version, no additional configuration is required! Apollo Client automatically sends the required headers with the request if the terminating `HTTPLink` is passed a subscription operation. +#### Usage with Relay or urql + +To consume a multipart subscription over HTTP in an app using Relay or urql, Apollo Client provides network layer adapters that handle the parsing of the multipart response format. + +##### Relay + +```ts +import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/relay"; +import { Environment, Network, RecordSource, Store } from "relay-runtime"; + +const fetchMultipartSubs = createFetchMultipartSubscription( + "https://api.example.com" +); + +const network = Network.create(fetchQuery, fetchMultipartSubs); + +export const RelayEnvironment = new Environment({ + network, + store: new Store(new RecordSource()), +}); +``` + +#### urql + +```ts +import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/urql"; +import { Client, fetchExchange, subscriptionExchange } from "@urql/core"; + +const url = "https://api.example.com"; + +const multipartSubscriptionForwarder = createFetchMultipartSubscription( + url +); + +const client = new Client({ + url, + exchanges: [ + fetchExchange, + subscriptionExchange({ + forwardSubscription: multipartSubscriptionForwarder, + }), + ], +}); +``` + ## Defining a subscription You define a subscription on both the server side and the client side, just like you do for queries and mutations. From a604ac39d54127ac4f45b7f0fafb41ad3f5ac9b5 Mon Sep 17 00:00:00 2001 From: Alessia Bellisario Date: Wed, 3 Jan 2024 17:31:08 -0500 Subject: [PATCH 77/90] chore: add docs for skipping an optimistic update via optimisticResponse (#11461) * chore: add docs for skipping an optimistic update via optimisticResponse * chore: make PR review updates --- docs/source/performance/optimistic-ui.mdx | 45 +++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/source/performance/optimistic-ui.mdx b/docs/source/performance/optimistic-ui.mdx index 35332dce9e2..a6b15b48efd 100644 --- a/docs/source/performance/optimistic-ui.mdx +++ b/docs/source/performance/optimistic-ui.mdx @@ -73,6 +73,51 @@ As this example shows, the value of `optimisticResponse` is an object that match 5. Apollo Client notifies all affected queries again. The associated components re-render, but if the server's response matches our `optimisticResponse`, this is invisible to the user. +## Bailing out of an optimistic update + +In some cases you may want to skip an optimistic update. For example, you may want to perform an optimistic update _only_ when certain variables are passed to the mutation. To skip an update, pass a function to the `optimisticResponse` option and return the `IGNORE` sentinel object available on the second argument to bail out of the optimistic update. + +Consider this example: + +```tsx +const UPDATE_COMMENT = gql` + mutation UpdateComment($commentId: ID!, $commentContent: String!) { + updateComment(commentId: $commentId, content: $commentContent) { + id + __typename + content + } + } +`; + +function CommentPageWithData() { + const [mutate] = useMutation(UPDATE_COMMENT); + + return ( + + mutate({ + variables: { commentId, commentContent }, + optimisticResponse: (vars, { IGNORE }) => { + if (commentContent === "foo") { + // conditionally bail out of optimistic updates + return IGNORE; + } + return { + updateComment: { + id: commentId, + __typename: "Comment", + content: commentContent + } + } + }, + }) + } + /> + ); +} +``` + ## Example: Adding a new object to a list The previous example shows how to provide an optimistic result for an object that's _already_ in the Apollo Client cache. But what about a mutation that creates a _new_ object? This works similarly. From 416db651c267689ab944527ad9d6a749ba979b9b Mon Sep 17 00:00:00 2001 From: Alessia Bellisario Date: Fri, 12 Jan 2024 15:34:37 -0500 Subject: [PATCH 78/90] chore: bump size limits, fix new lint error since merging main --- .size-limits.json | 4 ++-- src/react/hooks/internal/index.ts | 2 +- src/testing/core/mocking/mockLink.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index d284dce0aa7..899614463c5 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39129, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32663 + "dist/apollo-client.min.cjs": 39201, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32694 } diff --git a/src/react/hooks/internal/index.ts b/src/react/hooks/internal/index.ts index a3dc508d6d7..71cfbdc1799 100644 --- a/src/react/hooks/internal/index.ts +++ b/src/react/hooks/internal/index.ts @@ -3,4 +3,4 @@ export { useDeepMemo } from "./useDeepMemo.js"; export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js"; export { useRenderGuard } from "./useRenderGuard.js"; export { useLazyRef } from "./useLazyRef.js"; -export { __use } from "./__use.js"; \ No newline at end of file +export { __use } from "./__use.js"; diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index e155e28234b..6d4753746e5 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -143,8 +143,8 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} ); } } else { - if (response.maxUsageCount! > 1) { - response.maxUsageCount!--; + if (response.maxUsageCount && response.maxUsageCount > 1) { + response.maxUsageCount--; } else { mockedResponses.splice(responseIndex, 1); } From 48f73dc22e1c6a510addce0afd16cb6b075dce16 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 17 Jan 2024 09:29:09 -0700 Subject: [PATCH 79/90] Prepare for rc release --- .changeset/pre.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 4e9f230f135..b786f1dd150 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,6 +1,6 @@ { "mode": "pre", - "tag": "beta", + "tag": "rc", "initialVersions": { "@apollo/client": "3.8.3" }, diff --git a/package.json b/package.json index 144bb0ffa21..7ad9de3715c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.0-beta.1", + "version": "3.8.9", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 1190aa59a106217f7192c1f81099adfa5e4365c1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 17 Jan 2024 09:35:11 -0700 Subject: [PATCH 80/90] Increase default memory limits (#11495) --- .api-reports/api-report-utilities.md | 4 ++-- .changeset/pink-apricots-yawn.md | 5 +++++ .size-limits.json | 4 ++-- src/utilities/caching/sizes.ts | 4 ++-- 4 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 .changeset/pink-apricots-yawn.md diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 8028dbdf360..2a68877e70d 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -729,9 +729,9 @@ export const enum defaultCacheSizes { // (undocumented) "fragmentRegistry.transform" = 2000, // (undocumented) - "inMemoryCache.executeSelectionSet" = 10000, + "inMemoryCache.executeSelectionSet" = 50000, // (undocumented) - "inMemoryCache.executeSubSelectedArray" = 5000, + "inMemoryCache.executeSubSelectedArray" = 10000, // (undocumented) "inMemoryCache.maybeBroadcastWatch" = 5000, // (undocumented) diff --git a/.changeset/pink-apricots-yawn.md b/.changeset/pink-apricots-yawn.md new file mode 100644 index 00000000000..08ce8d75998 --- /dev/null +++ b/.changeset/pink-apricots-yawn.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Increase the default memory limits for `executeSelectionSet` and `executeSelectionSetArray`. diff --git a/.size-limits.json b/.size-limits.json index 899614463c5..b0991a503d4 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39201, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32694 + "dist/apollo-client.min.cjs": 39141, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32647 } diff --git a/src/utilities/caching/sizes.ts b/src/utilities/caching/sizes.ts index 28ace70a8c2..114ce2118bc 100644 --- a/src/utilities/caching/sizes.ts +++ b/src/utilities/caching/sizes.ts @@ -314,6 +314,6 @@ export const enum defaultCacheSizes { "cache.fragmentQueryDocuments" = 1000, "removeTypenameFromVariables.getVariableDefinitions" = 2000, "inMemoryCache.maybeBroadcastWatch" = 5000, - "inMemoryCache.executeSelectionSet" = 10000, - "inMemoryCache.executeSubSelectedArray" = 5000, + "inMemoryCache.executeSelectionSet" = 50000, + "inMemoryCache.executeSubSelectedArray" = 10000, } From d0afb458b2b878f3d757e55d4ab2a3bd20ccd7af Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 17 Jan 2024 09:37:21 -0700 Subject: [PATCH 81/90] Switch changeset to minor version bump --- .changeset/pink-apricots-yawn.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pink-apricots-yawn.md b/.changeset/pink-apricots-yawn.md index 08ce8d75998..6eec10853be 100644 --- a/.changeset/pink-apricots-yawn.md +++ b/.changeset/pink-apricots-yawn.md @@ -1,5 +1,5 @@ --- -"@apollo/client": patch +"@apollo/client": minor --- Increase the default memory limits for `executeSelectionSet` and `executeSelectionSetArray`. From 9f5f8013e749d115bb419a3854f0c22348f53d26 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 10:16:17 -0700 Subject: [PATCH 82/90] Version Packages (rc) (#11496) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 1 + CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index b786f1dd150..b3fac98f148 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -18,6 +18,7 @@ "hot-ducks-burn", "late-rabbits-protect", "mighty-coats-check", + "pink-apricots-yawn", "polite-avocados-warn", "quick-hats-marry", "rare-snakes-melt", diff --git a/CHANGELOG.md b/CHANGELOG.md index a896643d2eb..bb1a1d53c5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/client +## 3.9.0-rc.0 + +### Minor Changes + +- [#11495](https://github.com/apollographql/apollo-client/pull/11495) [`1190aa5`](https://github.com/apollographql/apollo-client/commit/1190aa59a106217f7192c1f81099adfa5e4365c1) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Increase the default memory limits for `executeSelectionSet` and `executeSelectionSetArray`. + ## 3.8.9 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 7b691b1ad80..173f8a7b6a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.0-beta.1", + "version": "3.9.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.0-beta.1", + "version": "3.9.0-rc.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 7ad9de3715c..f403a4560d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.8.9", + "version": "3.9.0-rc.0", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 67f62e359bc471787d066319326e5582b4a635c8 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 18 Jan 2024 11:25:58 -0700 Subject: [PATCH 83/90] Add changeset to bump rc version (#11503) --- .changeset/six-rocks-arrive.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/six-rocks-arrive.md diff --git a/.changeset/six-rocks-arrive.md b/.changeset/six-rocks-arrive.md new file mode 100644 index 00000000000..19b433d8439 --- /dev/null +++ b/.changeset/six-rocks-arrive.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Release changes from [`v3.8.10`](https://github.com/apollographql/apollo-client/releases/tag/v3.8.10) From cfe6b782fe3012495e76d08c38e31e8649f51445 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 18 Jan 2024 11:36:51 -0700 Subject: [PATCH 84/90] Version Packages (rc) (#11504) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 1 + CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index b3fac98f148..01678933c4d 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -24,6 +24,7 @@ "rare-snakes-melt", "shaggy-ears-scream", "shaggy-sheep-pull", + "six-rocks-arrive", "sixty-boxes-rest", "smooth-plums-shout", "sour-sheep-walk", diff --git a/CHANGELOG.md b/CHANGELOG.md index f8f6659642d..341779e9754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/client +## 3.9.0-rc.1 + +### Patch Changes + +- [#11503](https://github.com/apollographql/apollo-client/pull/11503) [`67f62e3`](https://github.com/apollographql/apollo-client/commit/67f62e359bc471787d066319326e5582b4a635c8) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Release changes from [`v3.8.10`](https://github.com/apollographql/apollo-client/releases/tag/v3.8.10) + ## 3.8.10 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 38d956fd15e..25229d1d759 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.0-rc.0", + "version": "3.9.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.0-rc.0", + "version": "3.9.0-rc.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 400a5a38f07..2859fe83766 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.0-rc.0", + "version": "3.9.0-rc.1", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 44640500540525895014ce8d97d338ca0e7884fa Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 22 Jan 2024 02:55:13 -0700 Subject: [PATCH 85/90] Add doc comment for `headers` property on ApolloClient (#11508) * Add doc comment for `headers` property on ApolloClient * Update src/core/ApolloClient.ts --------- Co-authored-by: Lenz Weber-Tronic --- src/core/ApolloClient.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 933d4266b1e..a0b33993d61 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -51,6 +51,11 @@ export interface ApolloClientOptions { */ uri?: string | UriFunction; credentials?: string; + /** + * An object representing headers to include in every HTTP request, such as `{Authorization: 'Bearer 1234'}` + * + * This value will be ignored when using the `link` option. + */ headers?: Record; /** * You can provide an {@link ApolloLink} instance to serve as Apollo Client's network layer. For more information, see [Advanced HTTP networking](https://www.apollographql.com/docs/react/networking/advanced-http-networking/). From 25b83dab6ec0f57d3ec47f76bdab546559d594df Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 22 Jan 2024 12:02:54 +0100 Subject: [PATCH 86/90] chore: update api-reports --- .api-reports/api-report-core.md | 1 - .api-reports/api-report-react.md | 1 - .api-reports/api-report-react_components.md | 1 - .api-reports/api-report-react_context.md | 1 - .api-reports/api-report-react_hoc.md | 1 - .api-reports/api-report-react_hooks.md | 1 - .api-reports/api-report-react_internal.md | 1 - .api-reports/api-report-react_ssr.md | 1 - .api-reports/api-report-testing.md | 1 - .api-reports/api-report-testing_core.md | 1 - .api-reports/api-report-utilities.md | 1 - .api-reports/api-report.md | 1 - 12 files changed, 12 deletions(-) diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index ab651c318da..5e267b7b567 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -155,7 +155,6 @@ export interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; link?: ApolloLink; name?: string; diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 14db26ad941..84109c58e2e 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -185,7 +185,6 @@ interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" link?: ApolloLink; diff --git a/.api-reports/api-report-react_components.md b/.api-reports/api-report-react_components.md index fed0f1ea60b..4908305f332 100644 --- a/.api-reports/api-report-react_components.md +++ b/.api-reports/api-report-react_components.md @@ -186,7 +186,6 @@ interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" link?: ApolloLink; diff --git a/.api-reports/api-report-react_context.md b/.api-reports/api-report-react_context.md index 01f8cfa71d1..dd5ecb6c320 100644 --- a/.api-reports/api-report-react_context.md +++ b/.api-reports/api-report-react_context.md @@ -185,7 +185,6 @@ interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" link?: ApolloLink; diff --git a/.api-reports/api-report-react_hoc.md b/.api-reports/api-report-react_hoc.md index 044cb10cc77..8c7591656ce 100644 --- a/.api-reports/api-report-react_hoc.md +++ b/.api-reports/api-report-react_hoc.md @@ -185,7 +185,6 @@ interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" link?: ApolloLink; diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index 3296d232fd0..1d10936cd2c 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -184,7 +184,6 @@ interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" link?: ApolloLink; diff --git a/.api-reports/api-report-react_internal.md b/.api-reports/api-report-react_internal.md index a54ec58ee63..436e8e0cb73 100644 --- a/.api-reports/api-report-react_internal.md +++ b/.api-reports/api-report-react_internal.md @@ -184,7 +184,6 @@ interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" link?: ApolloLink; diff --git a/.api-reports/api-report-react_ssr.md b/.api-reports/api-report-react_ssr.md index 8f22892c265..16bf49666da 100644 --- a/.api-reports/api-report-react_ssr.md +++ b/.api-reports/api-report-react_ssr.md @@ -185,7 +185,6 @@ interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" link?: ApolloLink; diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index 54327b058c4..79404c0510e 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -185,7 +185,6 @@ interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" link?: ApolloLink; diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 1b505f4d121..cee2394e945 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -184,7 +184,6 @@ interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" link?: ApolloLink; diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 2a68877e70d..5529eb18b85 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -197,7 +197,6 @@ interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" link?: ApolloLink; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 5decb483463..c88a0d6b1e3 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -157,7 +157,6 @@ export interface ApolloClientOptions { documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; link?: ApolloLink; name?: string; From 2a67ffc4e75745d4225fd26a95d0b3d39e448107 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 24 Jan 2024 18:25:54 +0100 Subject: [PATCH 87/90] move hook documentation (and others) into code, improve documentation components (#11381) * move some docs to comments * remove comments * fix missing space * add api_doc * tweak in file generation * build from docs branch * switch docs branch back to `main` * bump CI * Enum & PropertyDetails * WIP - see current state of `hooks.mdx` * completely remove `query-options` and `query-result` mdx * useMutation * handle subscription docs * better function signature * more hooks * fix up import * chores * formatting * suppress tsdoc warning * api explorer nitpicks * support for @deprecated * use prerendered markdown in comments to prevent client-side parsing * add propTypes * apply prettier * fixup * also display interface details for slightly complex types * heading styling, do not use h6 * since everywhere, styling, `link` defaults * add grouping, create documentation interface * `MutationOptionsDocumentation` interface * api reports * subscription options * api repots * prettier * fix some references * cleanup * fix up SubHeading links * don't add prefix to parameter properties * allow property deep linking * prettier * more cleanup * also add result documentation * more doccomment work * fixup * add missing import * fixup * remove `result` property (it's not a function) * Revert "remove `result` property (it's not a function)" This reverts commit 57c8526eb36d275f59811e5b27811f43ee1ba51c. * functions adjustments only show parameters section if there are parameters only show results if result is not `void` or user-specified * move heading out of example children * Update docs/shared/ApiDoc/EnumDetails.js Co-authored-by: Jerel Miller * remove obsolete props * address top padding for "smaller" headings * review feedback * fixup codeblock * move `SourceLink` out of `Heading` * throw an error if both `as` and `headingLevel` are specified in `Heading` * move jsx * always link headings * move headings out of table components * review comment * Update docs/shared/ApiDoc/Tuple.js Co-authored-by: Jerel Miller * nit :) * more updates * fix link --------- Co-authored-by: Jerel Miller --- .api-reports/api-report-cache.md | 2 +- .api-reports/api-report-core.md | 88 +-- .api-reports/api-report-react.md | 233 ++++--- .api-reports/api-report-react_components.md | 189 +++--- .api-reports/api-report-react_context.md | 152 +++-- .api-reports/api-report-react_hoc.md | 136 ++-- .api-reports/api-report-react_hooks.md | 234 ++++--- .api-reports/api-report-react_internal.md | 113 ++-- .api-reports/api-report-react_ssr.md | 153 +++-- .api-reports/api-report-testing.md | 113 ++-- .api-reports/api-report-testing_core.md | 113 ++-- .api-reports/api-report-utilities.md | 112 ++-- .api-reports/api-report.md | 203 +++--- api-extractor.json | 13 +- config/apiExtractor.ts | 8 +- config/entryPoints.js | 11 + config/inlineInheritDoc.ts | 35 +- docs/shared/ApiDoc/DocBlock.js | 57 +- docs/shared/ApiDoc/EnumDetails.js | 72 +++ docs/shared/ApiDoc/Function.js | 156 ++++- docs/shared/ApiDoc/Heading.js | 122 ++-- docs/shared/ApiDoc/InterfaceDetails.js | 24 +- docs/shared/ApiDoc/ParameterTable.js | 57 +- docs/shared/ApiDoc/PropertyDetails.js | 27 + docs/shared/ApiDoc/PropertySignatureTable.js | 152 ++--- docs/shared/ApiDoc/ResponsiveGrid.js | 8 +- docs/shared/ApiDoc/SourceLink.js | 24 + docs/shared/ApiDoc/Tuple.js | 68 ++ docs/shared/ApiDoc/getInterfaceReference.js | 8 + docs/shared/ApiDoc/index.js | 16 +- docs/shared/ApiDoc/mdToReact.js | 20 - docs/shared/ApiDoc/sortWithCustomOrder.js | 71 +++ docs/shared/apollo-provider.mdx | 1 - docs/shared/document-transform-options.mdx | 50 -- docs/shared/mutation-options.mdx | 298 --------- docs/shared/mutation-result.mdx | 145 ----- docs/shared/query-options.mdx | 304 --------- docs/shared/query-result.mdx | 275 -------- docs/shared/subscription-options.mdx | 14 - docs/shared/subscription-result.mdx | 5 - docs/shared/useSuspenseQuery-options.mdx | 217 ------- docs/source/api/core/ObservableQuery.mdx | 34 +- docs/source/api/react/components.mdx | 26 +- docs/source/api/react/hooks.mdx | 438 +++---------- docs/source/data/document-transforms.mdx | 29 +- docs/source/data/mutations.mdx | 10 +- docs/source/data/queries.mdx | 10 +- docs/source/data/subscriptions.mdx | 10 +- netlify.toml | 2 +- src/cache/core/types/DataProxy.ts | 24 +- src/cache/inmemory/types.ts | 1 - src/core/ApolloClient.ts | 2 +- src/core/ObservableQuery.ts | 22 + src/core/types.ts | 8 +- src/core/watchQueryOptions.ts | 242 ++----- src/react/components/types.ts | 1 + src/react/hooks/useApolloClient.ts | 15 + src/react/hooks/useLazyQuery.ts | 35 + src/react/hooks/useMutation.ts | 47 ++ src/react/hooks/useQuery.ts | 34 + src/react/hooks/useReactiveVar.ts | 17 + src/react/hooks/useSubscription.ts | 86 ++- .../query-preloader/createQueryPreloader.ts | 18 +- src/react/types/types.documentation.ts | 598 ++++++++++++++++++ src/react/types/types.ts | 252 +++++--- src/utilities/graphql/DocumentTransform.ts | 12 + tsdoc.json | 24 + 67 files changed, 2830 insertions(+), 3266 deletions(-) create mode 100644 docs/shared/ApiDoc/EnumDetails.js create mode 100644 docs/shared/ApiDoc/PropertyDetails.js create mode 100644 docs/shared/ApiDoc/SourceLink.js create mode 100644 docs/shared/ApiDoc/Tuple.js create mode 100644 docs/shared/ApiDoc/getInterfaceReference.js delete mode 100644 docs/shared/ApiDoc/mdToReact.js create mode 100644 docs/shared/ApiDoc/sortWithCustomOrder.js delete mode 100644 docs/shared/apollo-provider.mdx delete mode 100644 docs/shared/document-transform-options.mdx delete mode 100644 docs/shared/mutation-options.mdx delete mode 100644 docs/shared/mutation-result.mdx delete mode 100644 docs/shared/query-options.mdx delete mode 100644 docs/shared/query-result.mdx delete mode 100644 docs/shared/subscription-options.mdx delete mode 100644 docs/shared/subscription-result.mdx delete mode 100644 docs/shared/useSuspenseQuery-options.mdx create mode 100644 src/react/types/types.documentation.ts create mode 100644 tsdoc.json diff --git a/.api-reports/api-report-cache.md b/.api-reports/api-report-cache.md index 55957897f33..7eed64d3052 100644 --- a/.api-reports/api-report-cache.md +++ b/.api-reports/api-report-cache.md @@ -234,7 +234,7 @@ export namespace DataProxy { } // (undocumented) export interface ReadFragmentOptions extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index 5e267b7b567..24cf06f5a60 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -253,14 +253,18 @@ export interface ApolloPayloadResult, TExtensions = } // @public (undocumented) -export type ApolloQueryResult = { +export interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public (undocumented) export type ApolloReducerConfig = { @@ -479,7 +483,7 @@ export namespace DataProxy { } // (undocumented) export interface ReadFragmentOptions extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -591,9 +595,7 @@ export type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -766,9 +768,7 @@ export interface FetchMoreOptions export interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -1359,12 +1359,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export interface MutationOptions = ApolloCache> extends MutationBaseOptions { - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +export interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1382,6 +1380,14 @@ export type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1394,7 +1400,7 @@ interface MutationStoreValue { variables: Record; } -// @public (undocumented) +// @public @deprecated (undocumented) export type MutationUpdaterFn = (cache: ApolloCache, mutationResult: FetchResult) => void; @@ -1486,7 +1492,6 @@ export class ObservableQuery; }); - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1535,15 +1540,10 @@ export class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } @@ -1830,12 +1830,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; errorPolicy?: ErrorPolicy; fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -2012,6 +2013,26 @@ export type ServerParseError = Error & { export { setLogVerbosity } +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) export interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -2156,24 +2177,11 @@ export interface UriFunction { // @public (undocumented) export type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public -export interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - errorPolicy?: ErrorPolicy; - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +export interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // @public (undocumented) @@ -2214,13 +2222,13 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 84109c58e2e..e7a4e16d51c 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -322,14 +322,21 @@ interface ApolloProviderProps { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject = BackgroundQueryHookOptions, NoInfer>; +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export interface BaseMutationOptions = ApolloCache> extends Omit, "mutation"> { - // (undocumented) +export interface BaseMutationOptions = ApolloCache> extends MutationSharedOptions { client?: ApolloClient; - // (undocumented) ignoreResults?: boolean; - // (undocumented) notifyOnNetworkStatusChange?: boolean; - // (undocumented) onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; - // (undocumented) onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export interface BaseQueryOptions extends Omit, "query"> { - // (undocumented) +export interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: Context; - // (undocumented) ssr?: boolean; } // @public (undocumented) export interface BaseSubscriptionOptions { - // (undocumented) client?: ApolloClient; - // (undocumented) context?: Context; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchPolicy?: FetchPolicy; - // (undocumented) onComplete?: () => void; - // (undocumented) onData?: (options: OnDataOptions) => any; - // (undocumented) onError?: (error: ApolloError) => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionComplete?: () => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionData?: (options: OnSubscriptionDataOptions) => any; - // (undocumented) shouldResubscribe?: boolean | ((options: BaseSubscriptionOptions) => boolean); - // (undocumented) skip?: boolean; - // (undocumented) variables?: TVariables; } @@ -582,7 +576,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -713,11 +707,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts - // - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -792,9 +783,7 @@ type FetchMoreOptions = Parameters["fetchMore"]>[0 interface FetchMoreQueryOptions { // (undocumented) context?: Context; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -1042,14 +1031,21 @@ export interface LazyQueryHookExecOptions extends Omit, "skip"> { +export interface LazyQueryHookOptions extends BaseQueryOptions { + // @internal (undocumented) + defaultOptions?: Partial>; + onCompleted?: (data: TData) => void; + onError?: (error: ApolloError) => void; } // @public @deprecated (undocumented) export type LazyQueryResult = QueryResult; // @public (undocumented) -export type LazyQueryResultTuple = [LazyQueryExecFunction, QueryResult]; +export type LazyQueryResultTuple = [ +execute: LazyQueryExecFunction, +result: QueryResult +]; // @public (undocumented) type Listener = (promise: QueryRefPromise) => void; @@ -1059,18 +1055,16 @@ export type LoadableQueryHookFetchPolicy = Extract; context?: Context; // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; fetchPolicy?: LoadableQueryHookFetchPolicy; queryKey?: string | number | any[]; // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" refetchWritePolicy?: RefetchWritePolicy; returnPartialData?: boolean; } @@ -1179,7 +1173,6 @@ type Modifiers = Record> = Partia interface MutationBaseOptions = ApolloCache> { awaitRefetchQueries?: boolean; context?: TContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; // Warning: (ae-forgotten-export) The symbol "OnQueryUpdated" needs to be exported by the entry point index.d.ts onQueryUpdated?: OnQueryUpdated; @@ -1188,7 +1181,6 @@ interface MutationBaseOptions TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" @@ -1210,7 +1202,6 @@ export type MutationFunction = ApolloCache> extends BaseMutationOptions { - // (undocumented) mutation?: DocumentNode | TypedDocumentNode; } @@ -1218,14 +1209,8 @@ export interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { } -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -interface MutationOptions = ApolloCache> extends MutationBaseOptions { - // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1245,20 +1230,23 @@ type MutationQueryReducersMap { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data?: TData | null; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) reset(): void; } +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1273,8 +1261,8 @@ interface MutationStoreValue { // @public (undocumented) export type MutationTuple = ApolloCache> = [ -(options?: MutationFunctionOptions) => Promise>, -MutationResult +mutate: (options?: MutationFunctionOptions) => Promise>, +result: MutationResult ]; // @public (undocumented) @@ -1322,7 +1310,6 @@ class ObservableQuery; }); - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1371,22 +1358,31 @@ class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } // @public (undocumented) -export type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +export interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} // @public (undocumented) const OBSERVED_CHANGED_OPTIONS: readonly ["canonizeResults", "context", "errorPolicy", "fetchPolicy", "refetchWritePolicy", "returnPartialData"]; @@ -1529,19 +1525,15 @@ interface QueryData { export interface QueryDataOptions extends QueryFunctionOptions { // (undocumented) children?: (result: QueryResult) => ReactTypes.ReactNode; - // (undocumented) query: DocumentNode | TypedDocumentNode; } // @public (undocumented) -export interface QueryFunctionOptions extends BaseQueryOptions { - // (undocumented) +export interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1616,9 +1608,7 @@ interface QueryKey { // @public @deprecated (undocumented) export interface QueryLazyOptions { - // (undocumented) context?: Context; - // (undocumented) variables?: TVariables; } @@ -1751,14 +1741,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: Context; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1787,21 +1776,13 @@ type QueryRefPromise = PromiseWithState>; // @public (undocumented) export interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -1966,6 +1947,27 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: Context; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = Context, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -2039,7 +2041,6 @@ interface SubscriptionOptions { context?: Context; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -2047,13 +2048,10 @@ interface SubscriptionOptions { // @public (undocumented) export interface SubscriptionResult { - // (undocumented) data?: TData; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) + // @internal (undocumented) variables?: TVariables; } @@ -2061,13 +2059,20 @@ export interface SubscriptionResult { export type SuspenseQueryHookFetchPolicy = Extract; // @public (undocumented) -export interface SuspenseQueryHookOptions extends Pick, "client" | "variables" | "errorPolicy" | "context" | "canonizeResults" | "returnPartialData" | "refetchWritePolicy"> { - // (undocumented) +export interface SuspenseQueryHookOptions { + // @deprecated + canonizeResults?: boolean; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + client?: ApolloClient; + context?: Context; + errorPolicy?: ErrorPolicy; fetchPolicy?: SuspenseQueryHookFetchPolicy; - // (undocumented) queryKey?: string | number | any[]; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; // @deprecated skip?: boolean; + variables?: TVariables; } // @public (undocumented) @@ -2224,7 +2229,7 @@ export type UseFragmentResult = { missing?: MissingTree; }; -// @public (undocumented) +// @public export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; // @public (undocumented) @@ -2260,10 +2265,10 @@ QueryReference | null, } ]; -// @public (undocumented) +// @public export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; -// @public (undocumented) +// @public export function useQuery(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; // @public @@ -2271,15 +2276,13 @@ export function useQueryRefHandlers { - // (undocumented) fetchMore: FetchMoreFunction; - refetch: RefetchFunction; } // Warning: (ae-forgotten-export) The symbol "ReactiveVar" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export function useReactiveVar(rv: ReactiveVar): T; // @public (undocumented) @@ -2292,7 +2295,7 @@ export interface UseReadQueryResult { networkStatus: NetworkStatus; } -// @public (undocumented) +// @public export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer>): SubscriptionResult; // @public (undocumented) @@ -2371,55 +2374,31 @@ TVariables type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public -interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: Context; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" - errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts -// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:29:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useLoadableQuery.ts:49:5 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts -// src/react/query-preloader/createQueryPreloader.ts:80:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" -// src/react/query-preloader/createQueryPreloader.ts:85:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" -// src/react/query-preloader/createQueryPreloader.ts:95:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react_components.md b/.api-reports/api-report-react_components.md index 4908305f332..8307e63ec9c 100644 --- a/.api-reports/api-report-react_components.md +++ b/.api-reports/api-report-react_components.md @@ -287,14 +287,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject = ApolloCache> extends Omit, "mutation"> { +interface BaseMutationOptions = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts - // - // (undocumented) client?: ApolloClient; - // (undocumented) ignoreResults?: boolean; - // (undocumented) notifyOnNetworkStatusChange?: boolean; - // (undocumented) onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; - // (undocumented) onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface BaseQueryOptions extends Omit, "query"> { - // (undocumented) +interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } // @public (undocumented) interface BaseSubscriptionOptions { - // (undocumented) client?: ApolloClient; - // (undocumented) context?: DefaultContext; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchPolicy?: FetchPolicy; - // (undocumented) onComplete?: () => void; // Warning: (ae-forgotten-export) The symbol "OnDataOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) onData?: (options: OnDataOptions) => any; - // (undocumented) onError?: (error: ApolloError) => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionComplete?: () => void; // Warning: (ae-forgotten-export) The symbol "OnSubscriptionDataOptions" needs to be exported by the entry point index.d.ts // - // @deprecated (undocumented) + // @deprecated onSubscriptionData?: (options: OnSubscriptionDataOptions) => any; - // (undocumented) shouldResubscribe?: boolean | ((options: BaseSubscriptionOptions) => boolean); - // (undocumented) skip?: boolean; - // (undocumented) variables?: TVariables; } @@ -523,7 +515,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -624,11 +616,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts - // - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -679,9 +668,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -963,7 +950,6 @@ interface MutationBaseOptions; @@ -972,7 +958,6 @@ interface MutationBaseOptions TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" @@ -1003,18 +988,11 @@ type MutationFunction = ApolloCache> extends BaseMutationOptions { - // (undocumented) mutation?: DocumentNode | TypedDocumentNode; } -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -interface MutationOptions = ApolloCache> extends MutationBaseOptions { - // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1034,20 +1012,23 @@ type MutationQueryReducersMap { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data?: TData | null; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) reset(): void; } +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1105,8 +1086,6 @@ class ObservableQuery; }); // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1155,22 +1134,31 @@ class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } // @public (undocumented) -type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} // @public (undocumented) interface OnDataOptions { @@ -1245,14 +1233,11 @@ export interface QueryComponentOptions extends BaseQueryOptions { - // (undocumented) +interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1444,14 +1429,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1463,21 +1447,13 @@ interface QueryOptions { // // @public (undocumented) interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -1582,6 +1558,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1642,7 +1640,6 @@ export interface Subscription { export interface SubscriptionComponentOptions extends BaseSubscriptionOptions { // (undocumented) children?: null | ((result: SubscriptionResult) => ReactTypes.JSX.Element | null); - // (undocumented) subscription: DocumentNode | TypedDocumentNode; } @@ -1651,7 +1648,6 @@ interface SubscriptionOptions { context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1659,13 +1655,10 @@ interface SubscriptionOptions { // @public (undocumented) interface SubscriptionResult { - // (undocumented) data?: TData; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) + // @internal (undocumented) variables?: TVariables; } @@ -1723,50 +1716,28 @@ interface UriFunction { type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public -interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" - errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts -// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react_context.md b/.api-reports/api-report-react_context.md index dd5ecb6c320..337281bb134 100644 --- a/.api-reports/api-report-react_context.md +++ b/.api-reports/api-report-react_context.md @@ -318,14 +318,21 @@ export interface ApolloProviderProps { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject extends Omit, "query"> { - // (undocumented) +interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } @@ -506,7 +513,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -607,11 +614,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts - // - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -662,9 +666,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -934,7 +936,6 @@ interface MutationBaseOptions; @@ -943,7 +944,6 @@ interface MutationBaseOptions TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" @@ -956,14 +956,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface MutationOptions = ApolloCache> extends MutationBaseOptions { - // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -981,6 +977,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1038,8 +1043,6 @@ class ObservableQuery; }); // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1088,22 +1091,31 @@ class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } // @public (undocumented) -type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1146,21 +1158,17 @@ interface QueryDataOptions) => ReactTypes.ReactNode; - // (undocumented) query: DocumentNode | TypedDocumentNode; } // Warning: (ae-forgotten-export) The symbol "BaseQueryOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface QueryFunctionOptions extends BaseQueryOptions { - // (undocumented) +interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1352,14 +1360,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1371,21 +1378,13 @@ interface QueryOptions { // // @public (undocumented) interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -1515,6 +1514,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1559,7 +1580,6 @@ interface SubscriptionOptions { context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1619,50 +1639,28 @@ interface UriFunction { type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public -interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" - errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts -// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react_hoc.md b/.api-reports/api-report-react_hoc.md index 8c7591656ce..2be6017e01e 100644 --- a/.api-reports/api-report-react_hoc.md +++ b/.api-reports/api-report-react_hoc.md @@ -286,14 +286,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject = ApolloCache> extends Omit, "mutation"> { +interface BaseMutationOptions = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts - // - // (undocumented) client?: ApolloClient; - // (undocumented) ignoreResults?: boolean; - // (undocumented) notifyOnNetworkStatusChange?: boolean; - // (undocumented) onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; - // (undocumented) onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface BaseQueryOptions extends Omit, "query"> { - // (undocumented) +interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } @@ -505,7 +508,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -609,11 +612,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts - // - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -673,9 +673,7 @@ interface FetchMoreOptions { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -957,7 +955,6 @@ interface MutationBaseOptions; @@ -966,7 +963,6 @@ interface MutationBaseOptions TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" @@ -988,18 +984,11 @@ type MutationFunction = ApolloCache> extends BaseMutationOptions { - // (undocumented) mutation?: DocumentNode | TypedDocumentNode; } -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -interface MutationOptions = ApolloCache> extends MutationBaseOptions { - // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1019,20 +1008,23 @@ type MutationQueryReducersMap { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data?: TData | null; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) reset(): void; } +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1090,8 +1082,6 @@ class ObservableQuery; }); // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1140,17 +1130,11 @@ class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } @@ -1421,14 +1405,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1537,6 +1520,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1581,7 +1586,6 @@ interface SubscriptionOptions { context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1647,28 +1651,8 @@ interface UriFunction { type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public -interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" - errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // @public @deprecated (undocumented) @@ -1690,24 +1674,22 @@ export function withSubscription = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject = BackgroundQueryHookOptions, NoInfer>; +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface BaseMutationOptions = ApolloCache> extends Omit, "mutation"> { +interface BaseMutationOptions = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts - // - // (undocumented) client?: ApolloClient; - // (undocumented) ignoreResults?: boolean; - // (undocumented) notifyOnNetworkStatusChange?: boolean; - // (undocumented) onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; - // (undocumented) onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface BaseQueryOptions extends Omit, "query"> { - // (undocumented) +interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } // @public (undocumented) interface BaseSubscriptionOptions { - // (undocumented) client?: ApolloClient; - // (undocumented) context?: DefaultContext; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchPolicy?: FetchPolicy; - // (undocumented) onComplete?: () => void; // Warning: (ae-forgotten-export) The symbol "OnDataOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) onData?: (options: OnDataOptions) => any; - // (undocumented) onError?: (error: ApolloError) => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionComplete?: () => void; // Warning: (ae-forgotten-export) The symbol "OnSubscriptionDataOptions" needs to be exported by the entry point index.d.ts // - // @deprecated (undocumented) + // @deprecated onSubscriptionData?: (options: OnSubscriptionDataOptions) => any; - // (undocumented) shouldResubscribe?: boolean | ((options: BaseSubscriptionOptions) => boolean); - // (undocumented) skip?: boolean; - // (undocumented) variables?: TVariables; } @@ -546,7 +538,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -681,11 +673,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts - // - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -749,9 +738,7 @@ type FetchMoreOptions = Parameters["fetchMore"]>[0 interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -990,14 +977,23 @@ interface LazyQueryHookExecOptions; } +// Warning: (ae-forgotten-export) The symbol "BaseQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface LazyQueryHookOptions extends Omit, "skip"> { +interface LazyQueryHookOptions extends BaseQueryOptions { + // @internal (undocumented) + defaultOptions?: Partial>; + onCompleted?: (data: TData) => void; + onError?: (error: ApolloError) => void; } // Warning: (ae-forgotten-export) The symbol "LazyQueryExecFunction" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type LazyQueryResultTuple = [LazyQueryExecFunction, QueryResult]; +type LazyQueryResultTuple = [ +execute: LazyQueryExecFunction, +result: QueryResult +]; // @public (undocumented) type Listener = (promise: QueryRefPromise) => void; @@ -1007,19 +1003,17 @@ type LoadableQueryHookFetchPolicy = Extract; context?: DefaultContext; // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; // Warning: (ae-forgotten-export) The symbol "LoadableQueryHookFetchPolicy" needs to be exported by the entry point index.d.ts fetchPolicy?: LoadableQueryHookFetchPolicy; queryKey?: string | number | any[]; // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" refetchWritePolicy?: RefetchWritePolicy; returnPartialData?: boolean; } @@ -1128,7 +1122,6 @@ type Modifiers = Record> = Partia interface MutationBaseOptions = ApolloCache> { awaitRefetchQueries?: boolean; context?: TContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; // Warning: (ae-forgotten-export) The symbol "OnQueryUpdated" needs to be exported by the entry point index.d.ts onQueryUpdated?: OnQueryUpdated; @@ -1137,7 +1130,6 @@ interface MutationBaseOptions TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" @@ -1152,7 +1144,6 @@ type MutationFetchPolicy = Extract; // // @public (undocumented) interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { - // (undocumented) mutation?: DocumentNode | TypedDocumentNode; } @@ -1160,14 +1151,8 @@ interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { } -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -interface MutationOptions = ApolloCache> extends MutationBaseOptions { - // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1187,20 +1172,23 @@ type MutationQueryReducersMap { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data?: TData | null; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) reset(): void; } +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1218,8 +1206,8 @@ interface MutationStoreValue { // // @public (undocumented) type MutationTuple = ApolloCache> = [ -(options?: MutationFunctionOptions) => Promise>, -MutationResult +mutate: (options?: MutationFunctionOptions) => Promise>, +result: MutationResult ]; // @public (undocumented) @@ -1267,7 +1255,6 @@ class ObservableQuery; }); - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1316,22 +1303,31 @@ class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } // @public (undocumented) -type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} // @public (undocumented) const OBSERVED_CHANGED_OPTIONS: readonly ["canonizeResults", "context", "errorPolicy", "fetchPolicy", "refetchWritePolicy", "returnPartialData"]; @@ -1411,17 +1407,12 @@ type PromiseWithState = PendingPromise | FulfilledPromise extends BaseQueryOptions { - // (undocumented) +interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1625,14 +1616,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1663,21 +1653,13 @@ type QueryRefPromise = PromiseWithState>; // // @public (undocumented) interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -1813,6 +1795,27 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1872,7 +1875,6 @@ interface SubscriptionOptions { context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1880,13 +1882,10 @@ interface SubscriptionOptions { // @public (undocumented) interface SubscriptionResult { - // (undocumented) data?: TData; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) + // @internal (undocumented) variables?: TVariables; } @@ -1894,15 +1893,21 @@ interface SubscriptionResult { type SuspenseQueryHookFetchPolicy = Extract; // @public (undocumented) -interface SuspenseQueryHookOptions extends Pick, "client" | "variables" | "errorPolicy" | "context" | "canonizeResults" | "returnPartialData" | "refetchWritePolicy"> { +interface SuspenseQueryHookOptions { + // @deprecated + canonizeResults?: boolean; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + client?: ApolloClient; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; // Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookFetchPolicy" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchPolicy?: SuspenseQueryHookFetchPolicy; - // (undocumented) queryKey?: string | number | any[]; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; // @deprecated skip?: boolean; + variables?: TVariables; } // @public (undocumented) @@ -2062,7 +2067,7 @@ export type UseFragmentResult = { // Warning: (ae-forgotten-export) The symbol "LazyQueryResultTuple" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; // Warning: (ae-forgotten-export) The symbol "LoadableQueryHookOptions" needs to be exported by the entry point index.d.ts @@ -2103,10 +2108,10 @@ QueryReference | null, // Warning: (ae-forgotten-export) The symbol "MutationHookOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationTuple" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; -// @public (undocumented) +// @public export function useQuery(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; // @public @@ -2114,15 +2119,13 @@ export function useQueryRefHandlers { - // (undocumented) fetchMore: FetchMoreFunction; - refetch: RefetchFunction; } // Warning: (ae-forgotten-export) The symbol "ReactiveVar" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export function useReactiveVar(rv: ReactiveVar): T; // @public (undocumented) @@ -2137,7 +2140,7 @@ export interface UseReadQueryResult { // Warning: (ae-forgotten-export) The symbol "SubscriptionHookOptions" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer>): SubscriptionResult; // Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookOptions" needs to be exported by the entry point index.d.ts @@ -2207,49 +2210,28 @@ export interface UseSuspenseQueryResult { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" - errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts -// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:29:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useLoadableQuery.ts:49:5 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react_internal.md b/.api-reports/api-report-react_internal.md index 436e8e0cb73..258f0f992b9 100644 --- a/.api-reports/api-report-react_internal.md +++ b/.api-reports/api-report-react_internal.md @@ -285,14 +285,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -571,11 +578,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts - // - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -629,9 +633,7 @@ type FetchMoreOptions = Parameters["fetchMore"]>[0 interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -968,7 +970,6 @@ interface MutationBaseOptions; @@ -977,7 +978,6 @@ interface MutationBaseOptions TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" @@ -990,14 +990,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface MutationOptions = ApolloCache> extends MutationBaseOptions { - // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1015,6 +1011,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1072,8 +1077,6 @@ class ObservableQuery; }); // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1122,17 +1125,11 @@ class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } @@ -1382,14 +1379,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1524,6 +1520,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1568,7 +1586,6 @@ interface SubscriptionOptions { context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1648,29 +1665,11 @@ interface UriFunction { // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public -interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" - errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // @public (undocumented) @@ -1678,24 +1677,22 @@ export function wrapQueryRef(inter // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts -// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react_ssr.md b/.api-reports/api-report-react_ssr.md index 16bf49666da..a6057e4a7eb 100644 --- a/.api-reports/api-report-react_ssr.md +++ b/.api-reports/api-report-react_ssr.md @@ -286,14 +286,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject extends Omit, "query"> { +interface BaseQueryOptions extends SharedWatchQueryOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts - // - // (undocumented) + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } @@ -476,7 +482,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -577,11 +583,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts - // - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -632,9 +635,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -920,7 +921,6 @@ interface MutationBaseOptions; @@ -929,7 +929,6 @@ interface MutationBaseOptions TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" @@ -942,14 +941,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface MutationOptions = ApolloCache> extends MutationBaseOptions { - // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -967,6 +962,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1024,8 +1028,6 @@ class ObservableQuery; }); // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1074,22 +1076,31 @@ class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } // @public (undocumented) -type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1132,21 +1143,17 @@ interface QueryDataOptions) => ReactTypes.ReactNode; - // (undocumented) query: DocumentNode | TypedDocumentNode; } // Warning: (ae-forgotten-export) The symbol "BaseQueryOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface QueryFunctionOptions extends BaseQueryOptions { - // (undocumented) +interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1338,14 +1345,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1357,21 +1363,13 @@ interface QueryOptions { // // @public (undocumented) interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -1501,6 +1499,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1545,7 +1565,6 @@ interface SubscriptionOptions { context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1605,50 +1624,28 @@ interface UriFunction { type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public -interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" - errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts -// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index 79404c0510e..5b4ae8e658b 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -286,14 +286,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -571,11 +578,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts - // - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -626,9 +630,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -1034,7 +1036,6 @@ interface MutationBaseOptions; @@ -1043,7 +1044,6 @@ interface MutationBaseOptions TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" @@ -1056,14 +1056,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface MutationOptions = ApolloCache> extends MutationBaseOptions { - // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1081,6 +1077,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1148,8 +1153,6 @@ class ObservableQuery; }); // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1198,17 +1201,11 @@ class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } @@ -1423,14 +1420,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1542,6 +1538,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1589,7 +1607,6 @@ interface SubscriptionOptions { context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1657,29 +1674,11 @@ export function wait(ms: number): Promise; // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public -interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" - errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // @public @deprecated (undocumented) @@ -1693,24 +1692,22 @@ export function withWarningSpy(it: (...args: TArgs // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts -// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index cee2394e945..202108f79c7 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -285,14 +285,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -570,11 +577,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts - // - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -625,9 +629,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -989,7 +991,6 @@ interface MutationBaseOptions; @@ -998,7 +999,6 @@ interface MutationBaseOptions TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" @@ -1011,14 +1011,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface MutationOptions = ApolloCache> extends MutationBaseOptions { - // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1036,6 +1032,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1103,8 +1108,6 @@ class ObservableQuery; }); // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1153,17 +1156,11 @@ class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } @@ -1380,14 +1377,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1499,6 +1495,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1546,7 +1564,6 @@ interface SubscriptionOptions { context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1614,29 +1631,11 @@ export function wait(ms: number): Promise; // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public -interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" - errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // @public @deprecated (undocumented) @@ -1650,24 +1649,22 @@ export function withWarningSpy(it: (...args: TArgs // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts -// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 5529eb18b85..715d0827a56 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -309,14 +309,21 @@ interface ApolloPayloadResult, TExtensions = Record< } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public (undocumented) type ApolloReducerConfig = { @@ -606,7 +613,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -815,9 +822,7 @@ export type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -970,9 +975,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -1675,7 +1678,6 @@ interface MutationBaseOptions; @@ -1684,7 +1686,6 @@ interface MutationBaseOptions TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" update?: MutationUpdaterFunction; // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" @@ -1697,14 +1698,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface MutationOptions = ApolloCache> extends MutationBaseOptions { - // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1722,6 +1719,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1824,8 +1830,6 @@ class ObservableQuery; }); // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1872,17 +1876,11 @@ class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } @@ -2185,14 +2183,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -2383,6 +2380,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) export function shouldInclude({ directives }: SelectionNode, variables?: Record): boolean; @@ -2449,7 +2468,6 @@ interface SubscriptionOptions { context?: DefaultContext; // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -2562,29 +2580,11 @@ export type VariableValue = (node: VariableNode) => any; // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public -interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" - errorPolicy?: ErrorPolicy; - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts - // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // @public (undocumented) @@ -2622,7 +2622,7 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // -// src/cache/core/types/DataProxy.ts:153:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:57:3 - (ae-forgotten-export) The symbol "TypePolicy" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts @@ -2631,17 +2631,15 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/writeToStore.ts:65:7 - (ae-forgotten-export) The symbol "MergeTree" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts -// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/utilities/graphql/storeUtils.ts:226:12 - (ae-forgotten-export) The symbol "storeKeyNameStringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index c88a0d6b1e3..39d55033f88 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -289,14 +289,18 @@ interface ApolloProviderProps { } // @public (undocumented) -export type ApolloQueryResult = { +export interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public (undocumented) export type ApolloReducerConfig = { @@ -327,53 +331,40 @@ export interface BackgroundQueryHookOptions = BackgroundQueryHookOptions, NoInfer>; +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export interface BaseMutationOptions = ApolloCache> extends Omit, "mutation"> { - // (undocumented) +export interface BaseMutationOptions = ApolloCache> extends MutationSharedOptions { client?: ApolloClient; - // (undocumented) ignoreResults?: boolean; - // (undocumented) notifyOnNetworkStatusChange?: boolean; - // (undocumented) onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; - // (undocumented) onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export interface BaseQueryOptions extends Omit, "query"> { - // (undocumented) +export interface BaseQueryOptions extends SharedWatchQueryOptions { client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } // @public (undocumented) export interface BaseSubscriptionOptions { - // (undocumented) client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) fetchPolicy?: FetchPolicy; - // (undocumented) onComplete?: () => void; - // (undocumented) onData?: (options: OnDataOptions) => any; - // (undocumented) onError?: (error: ApolloError) => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionComplete?: () => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionData?: (options: OnSubscriptionDataOptions) => any; - // (undocumented) shouldResubscribe?: boolean | ((options: BaseSubscriptionOptions) => boolean); - // (undocumented) skip?: boolean; - // (undocumented) variables?: TVariables; } @@ -589,7 +580,7 @@ export namespace DataProxy { } // (undocumented) export interface ReadFragmentOptions extends Fragment { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -737,9 +728,7 @@ export type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -934,9 +923,7 @@ type FetchMoreOptions_2 = Parameters["fetchMore"]> export interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -1458,14 +1445,21 @@ export interface LazyQueryHookExecOptions extends Omit, "skip"> { +export interface LazyQueryHookOptions extends BaseQueryOptions { + // @internal (undocumented) + defaultOptions?: Partial>; + onCompleted?: (data: TData) => void; + onError?: (error: ApolloError) => void; } // @public @deprecated (undocumented) export type LazyQueryResult = QueryResult; // @public (undocumented) -export type LazyQueryResultTuple = [LazyQueryExecFunction, QueryResult]; +export type LazyQueryResultTuple = [ +execute: LazyQueryExecFunction, +result: QueryResult +]; // @public (undocumented) type Listener = (promise: QueryRefPromise) => void; @@ -1475,7 +1469,7 @@ export type LoadableQueryHookFetchPolicy = Extract; context?: DefaultContext; @@ -1652,7 +1646,6 @@ export type MutationFunction = ApolloCache> extends BaseMutationOptions { - // (undocumented) mutation?: DocumentNode | TypedDocumentNode; } @@ -1660,12 +1653,8 @@ export interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { } -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -export interface MutationOptions = ApolloCache> extends MutationBaseOptions { - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +export interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1685,20 +1674,22 @@ export type MutationQueryReducersMap { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data?: TData | null; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) reset(): void; } +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1713,11 +1704,11 @@ interface MutationStoreValue { // @public (undocumented) export type MutationTuple = ApolloCache> = [ -(options?: MutationFunctionOptions) => Promise>, -MutationResult +mutate: (options?: MutationFunctionOptions) => Promise>, +result: MutationResult ]; -// @public (undocumented) +// @public @deprecated (undocumented) export type MutationUpdaterFn = (cache: ApolloCache, mutationResult: FetchResult) => void; @@ -1812,7 +1803,6 @@ export class ObservableQuery; }); - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1861,20 +1851,30 @@ export class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } // @public (undocumented) -export type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +export interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} export { ObservableSubscription } @@ -2094,19 +2094,15 @@ interface QueryData { export interface QueryDataOptions extends QueryFunctionOptions { // (undocumented) children?: (result: QueryResult) => ReactTypes.ReactNode; - // (undocumented) query: DocumentNode | TypedDocumentNode; } // @public (undocumented) -export interface QueryFunctionOptions extends BaseQueryOptions { - // (undocumented) +export interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -2179,9 +2175,7 @@ interface QueryKey { // @public @deprecated (undocumented) export interface QueryLazyOptions { - // (undocumented) context?: DefaultContext; - // (undocumented) variables?: TVariables; } @@ -2309,12 +2303,13 @@ class QueryManager { // @public interface QueryOptions { - // @deprecated (undocumented) + // @deprecated canonizeResults?: boolean; context?: DefaultContext; errorPolicy?: ErrorPolicy; fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -2345,21 +2340,13 @@ type QueryRefPromise = PromiseWithState>; // @public (undocumented) export interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -2573,6 +2560,26 @@ export type ServerParseError = Error & { export { setLogVerbosity } +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) export interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -2664,13 +2671,10 @@ export interface SubscriptionOptions { - // (undocumented) data?: TData; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) + // @internal (undocumented) variables?: TVariables; } @@ -2678,13 +2682,19 @@ export interface SubscriptionResult { export type SuspenseQueryHookFetchPolicy = Extract; // @public (undocumented) -export interface SuspenseQueryHookOptions extends Pick, "client" | "variables" | "errorPolicy" | "context" | "canonizeResults" | "returnPartialData" | "refetchWritePolicy"> { - // (undocumented) +export interface SuspenseQueryHookOptions { + // @deprecated + canonizeResults?: boolean; + client?: ApolloClient; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; fetchPolicy?: SuspenseQueryHookFetchPolicy; - // (undocumented) queryKey?: string | number | any[]; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; // @deprecated skip?: boolean; + variables?: TVariables; } // @public (undocumented) @@ -2872,7 +2882,7 @@ export type UseFragmentResult = { missing?: MissingTree; }; -// @public (undocumented) +// @public export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; // @public (undocumented) @@ -2908,10 +2918,10 @@ QueryReference | null, } ]; -// @public (undocumented) +// @public export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; -// @public (undocumented) +// @public export function useQuery(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; // @public @@ -2919,13 +2929,11 @@ export function useQueryRefHandlers { - // (undocumented) fetchMore: FetchMoreFunction; - refetch: RefetchFunction; } -// @public (undocumented) +// @public export function useReactiveVar(rv: ReactiveVar): T; // @public (undocumented) @@ -2938,7 +2946,7 @@ export interface UseReadQueryResult { networkStatus: NetworkStatus; } -// @public (undocumented) +// @public export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer>): SubscriptionResult; // @public (undocumented) @@ -3017,23 +3025,8 @@ TVariables export type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; // @public -export interface WatchQueryOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; - context?: DefaultContext; - errorPolicy?: ErrorPolicy; - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +export interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - skipPollAttempt?: () => boolean; - variables?: TVariables; } // @public (undocumented) @@ -3074,13 +3067,13 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:3 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:316:3 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:29:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts diff --git a/api-extractor.json b/api-extractor.json index b257d21f772..d026e304ed8 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -128,13 +128,12 @@ "logLevel": "warning", "addToApiReportFile": true } + }, + "tsdocMessageReporting": { + "tsdoc-escape-greater-than": { + "logLevel": "none", + "addToApiReportFile": false + } } - - // "ae-extra-release-tag": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . } } diff --git a/config/apiExtractor.ts b/config/apiExtractor.ts index f64b0d7b525..b902353a14c 100644 --- a/config/apiExtractor.ts +++ b/config/apiExtractor.ts @@ -9,7 +9,7 @@ import { parseArgs } from "node:util"; import fs from "node:fs"; // @ts-ignore -import { map } from "./entryPoints.js"; +import { map, buildDocEntryPoints } from "./entryPoints.js"; import { readFileSync } from "fs"; const parsed = parseArgs({ @@ -47,12 +47,8 @@ try { console.log( "\n\nCreating API extractor docmodel for the a combination of all entry points" ); - const dist = path.resolve(__dirname, "../dist"); - const entryPoints = map((entryPoint: { dirs: string[] }) => { - return `export * from "${dist}/${entryPoint.dirs.join("/")}/index.d.ts";`; - }).join("\n"); const entryPointFile = path.join(tempDir, "entry.d.ts"); - fs.writeFileSync(entryPointFile, entryPoints); + fs.writeFileSync(entryPointFile, buildDocEntryPoints()); buildReport(entryPointFile, "docModel"); } diff --git a/config/entryPoints.js b/config/entryPoints.js index 896f87acf9f..cad194d61aa 100644 --- a/config/entryPoints.js +++ b/config/entryPoints.js @@ -126,3 +126,14 @@ function arraysEqualUpTo(a, b, end) { } return true; } + +exports.buildDocEntryPoints = () => { + const dist = path.resolve(__dirname, "../dist"); + const entryPoints = exports.map((entryPoint) => { + return `export * from "${dist}/${entryPoint.dirs.join("/")}/index.d.ts";`; + }); + entryPoints.push( + `export * from "${dist}/react/types/types.documentation.ts";` + ); + return entryPoints.join("\n"); +}; diff --git a/config/inlineInheritDoc.ts b/config/inlineInheritDoc.ts index 5c94a53363c..704054f28ec 100644 --- a/config/inlineInheritDoc.ts +++ b/config/inlineInheritDoc.ts @@ -23,10 +23,13 @@ */ /** End file docs */ +// @ts-ignore +import { buildDocEntryPoints } from "./entryPoints.js"; // @ts-ignore import { Project, ts, printNode, Node } from "ts-morph"; import { ApiModel, ApiDocumentedItem } from "@microsoft/api-extractor-model"; import { DeclarationReference } from "@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference"; +import { StringBuilder, TSDocEmitter } from "@microsoft/tsdoc"; import fs from "node:fs"; import path from "node:path"; @@ -53,7 +56,12 @@ function getCommentFor(canonicalReference: string) { `Could not resolve canonical reference "${canonicalReference}"` ); if (apiItem instanceof ApiDocumentedItem) { - return apiItem.tsdocComment?.emitAsTsdoc(); + if (!apiItem.tsdocComment) return ""; + const stringBuilder = new StringBuilder(); + const emitter = new TSDocEmitter(); + emitter["_emitCommentFraming"] = false; + emitter["_renderCompleteObject"](stringBuilder, apiItem.tsdocComment); + return stringBuilder.toString(); } else { throw new Error( `"${canonicalReference}" is not documented, so no documentation can be inherited.` @@ -64,6 +72,9 @@ function getCommentFor(canonicalReference: string) { function loadApiModel() { const tempDir = fs.mkdtempSync("api-model"); try { + const entryPointFile = path.join(tempDir, "entry.d.ts"); + fs.writeFileSync(entryPointFile, buildDocEntryPoints()); + // Load and parse the api-extractor.json file const configObjectFullPath = path.resolve( __dirname, @@ -73,6 +84,7 @@ function loadApiModel() { const tempModelFile = path.join(tempDir, "client.api.json"); const configObject = ExtractorConfig.loadFile(configObjectFullPath); + configObject.mainEntryPointFilePath = entryPointFile; configObject.docModel = { ...configObject.docModel, enabled: true, @@ -136,17 +148,24 @@ function processComments() { const docsNode = node.getJsDocs()[0]; if (!docsNode) return; const oldText = docsNode.getInnerText(); - const newText = oldText.replace( - inheritDocRegex, - (_, canonicalReference) => { - return getCommentFor(canonicalReference) || ""; - } - ); + let newText = oldText; + while (inheritDocRegex.test(newText)) { + newText = newText.replace( + inheritDocRegex, + (_, canonicalReference) => { + return getCommentFor(canonicalReference) || ""; + } + ); + } if (oldText !== newText) { - docsNode.replaceWithText(newText); + docsNode.replaceWithText(frameComment(newText)) as any; } } }); file.saveSync(); } } + +function frameComment(text: string) { + return `/**\n * ${text.trim().replace(/\n/g, "\n * ")}\n */`; +} diff --git a/docs/shared/ApiDoc/DocBlock.js b/docs/shared/ApiDoc/DocBlock.js index 157ece36fdc..18e29d26d19 100644 --- a/docs/shared/ApiDoc/DocBlock.js +++ b/docs/shared/ApiDoc/DocBlock.js @@ -1,27 +1,24 @@ import PropTypes from "prop-types"; import React from "react"; import { Stack } from "@chakra-ui/react"; -import { mdToReact } from "./mdToReact"; import { useApiDocContext } from "."; +import { useMDXComponents } from "@mdx-js/react"; export function DocBlock({ canonicalReference, summary = true, remarks = false, example = false, - remarkCollapsible = true, - since = true, - deprecated = true, + remarksCollapsible = false, + deprecated = false, }) { return ( - {/** TODO: @since, @deprecated etc. */} {deprecated && } - {since && } {summary && } {remarks && ( )} @@ -35,8 +32,7 @@ DocBlock.propTypes = { summary: PropTypes.bool, remarks: PropTypes.bool, example: PropTypes.bool, - remarkCollapsible: PropTypes.bool, - since: PropTypes.bool, + remarksCollapsible: PropTypes.bool, deprecated: PropTypes.bool, }; @@ -57,17 +53,18 @@ MaybeCollapsible.propTypes = { children: PropTypes.node, }; -/** - * Might still need more work on the Gatsby side to get this to work. - */ export function Deprecated({ canonicalReference, collapsible = false }) { const getItem = useApiDocContext(); const item = getItem(canonicalReference); + const MDX = useMDXComponents(); const value = item.comment?.deprecated; if (!value) return null; return ( - {mdToReact(value)} + +

⚠️ Deprecated

+ {value} +
); } @@ -76,33 +73,15 @@ Deprecated.propTypes = { collapsible: PropTypes.bool, }; -/** - * Might still need more work on the Gatsby side to get this to work. - */ -export function Since({ canonicalReference, collapsible = false }) { - const getItem = useApiDocContext(); - const item = getItem(canonicalReference); - const value = item.comment?.since; - if (!value) return null; - return ( - - Added to Apollo Client in version {value} - - ); -} -Since.propTypes = { - canonicalReference: PropTypes.string.isRequired, - collapsible: PropTypes.bool, -}; - export function Summary({ canonicalReference, collapsible = false }) { const getItem = useApiDocContext(); const item = getItem(canonicalReference); + const MDX = useMDXComponents(); const value = item.comment?.summary; if (!value) return null; return ( - {mdToReact(value)} + {value && {value}} ); } @@ -114,11 +93,12 @@ Summary.propTypes = { export function Remarks({ canonicalReference, collapsible = false }) { const getItem = useApiDocContext(); const item = getItem(canonicalReference); + const MDX = useMDXComponents(); const value = item.comment?.remarks?.replace(/^@remarks/g, ""); if (!value) return null; return ( - {mdToReact(value)} + {value && {value}} ); } @@ -134,12 +114,15 @@ export function Example({ }) { const getItem = useApiDocContext(); const item = getItem(canonicalReference); + const MDX = useMDXComponents(); const value = item.comment?.examples[index]; if (!value) return null; return ( - - {mdToReact(value)} - + <> + + {value && {value}} + + ); } Example.propTypes = { diff --git a/docs/shared/ApiDoc/EnumDetails.js b/docs/shared/ApiDoc/EnumDetails.js new file mode 100644 index 00000000000..a0f7966f55e --- /dev/null +++ b/docs/shared/ApiDoc/EnumDetails.js @@ -0,0 +1,72 @@ +import { useMDXComponents } from "@mdx-js/react"; + +import PropTypes from "prop-types"; +import React, { useMemo } from "react"; +import { DocBlock, useApiDocContext, ApiDocHeading, SectionHeading } from "."; +import { GridItem, Text } from "@chakra-ui/react"; +import { ResponsiveGrid } from "./ResponsiveGrid"; +import { sortWithCustomOrder } from "./sortWithCustomOrder"; + +export function EnumDetails({ + canonicalReference, + headingLevel, + customOrder = [], +}) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + + const sortedMembers = useMemo( + () => item.members.map(getItem).sort(sortWithCustomOrder(customOrder)), + [item.members, getItem, customOrder] + ); + + return ( + <> + + + + + Enumeration Members + + + + {sortedMembers.map((member) => ( + + + + + ))} + + + ); +} + +EnumDetails.propTypes = { + canonicalReference: PropTypes.string.isRequired, + headingLevel: PropTypes.number.isRequired, + customOrder: PropTypes.arrayOf(PropTypes.string), +}; diff --git a/docs/shared/ApiDoc/Function.js b/docs/shared/ApiDoc/Function.js index 97cb0934ce8..7cf0e8cbc3d 100644 --- a/docs/shared/ApiDoc/Function.js +++ b/docs/shared/ApiDoc/Function.js @@ -1,34 +1,55 @@ import PropTypes from "prop-types"; import React from "react"; -import { ApiDocHeading, DocBlock, ParameterTable, useApiDocContext } from "."; - +import { useMDXComponents } from "@mdx-js/react"; +import { + ApiDocHeading, + SubHeading, + DocBlock, + ParameterTable, + useApiDocContext, + PropertySignatureTable, + SourceLink, + Example, + getInterfaceReference, +} from "."; +import { GridItem } from "@chakra-ui/react"; export function FunctionSignature({ canonicalReference, parameterTypes = false, name = true, arrow = false, + highlight = false, }) { + const MDX = useMDXComponents(); const getItem = useApiDocContext(); const { displayName, parameters, returnType } = getItem(canonicalReference); - return ( - <> - {name ? displayName : ""}( - {parameters - .map((p) => { - let pStr = p.name; - if (p.optional) { - pStr += "?"; - } - if (parameterTypes) { - pStr += ": " + p.type; - } - return pStr; - }) - .join(", ")} - ){arrow ? " =>" : ":"} {returnType} - - ); + let paramSignature = parameters + .map((p) => { + let pStr = p.name; + if (p.optional) { + pStr += "?"; + } + if (parameterTypes) { + pStr += ": " + p.type; + } + return pStr; + }) + .join(",\n "); + + if (paramSignature) { + paramSignature = "\n " + paramSignature + "\n"; + } + + const signature = `${arrow ? "" : "function "}${ + name ? displayName : "" + }(${paramSignature})${arrow ? " =>" : ":"} ${returnType}`; + + return highlight ? + + {signature} + + : signature; } FunctionSignature.propTypes = { @@ -36,29 +57,109 @@ FunctionSignature.propTypes = { parameterTypes: PropTypes.bool, name: PropTypes.bool, arrow: PropTypes.bool, + highlight: PropTypes.bool, +}; + +export function ReturnType({ canonicalReference }) { + const MDX = useMDXComponents(); + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + + const interfaceReference = getInterfaceReference( + item.returnType, + item, + getItem + ); + return ( + <> + {item.comment?.returns} + + {item.returnType} + + {interfaceReference ? +
+ + Show/hide child attributes + + +
+ : null} + + ); +} +ReturnType.propTypes = { + canonicalReference: PropTypes.string.isRequired, }; export function FunctionDetails({ canonicalReference, customParameterOrder, headingLevel, + result, }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); return ( <> - + {item.comment?.examples.length == 0 ? null : ( + <> + + Example + + + + )} + - + Signature + + + + {item.parameters.length == 0 ? null : ( + <> + + Parameters + + + + )} + {( + result === false || (result === undefined && item.returnType === "void") + ) ? + null + : <> + + Result + + {result || } + } ); } @@ -67,4 +168,5 @@ FunctionDetails.propTypes = { canonicalReference: PropTypes.string.isRequired, headingLevel: PropTypes.number.isRequired, customParameterOrder: PropTypes.arrayOf(PropTypes.string), + result: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]), }; diff --git a/docs/shared/ApiDoc/Heading.js b/docs/shared/ApiDoc/Heading.js index e4a5aa9db69..fea39f5c9f4 100644 --- a/docs/shared/ApiDoc/Heading.js +++ b/docs/shared/ApiDoc/Heading.js @@ -1,67 +1,117 @@ import { useMDXComponents } from "@mdx-js/react"; import PropTypes from "prop-types"; import React from "react"; -import { Box, Heading } from "@chakra-ui/react"; +import { Box, Text } from "@chakra-ui/react"; import { FunctionSignature } from "."; import { useApiDocContext } from "./Context"; -const levels = { - 2: "xl", - 3: "lg", - 4: "md", - 5: "sm", - 6: "xs", +export function Heading({ headingLevel, children, as, minVersion, ...props }) { + const MDX = useMDXComponents(); + let heading = children; + + if (as != undefined && headingLevel != undefined) { + throw new Error( + "Heading: Cannot specify both `as` and `headingLevel` at the same time." + ); + } + const Tag = as ? as : MDX[`h${headingLevel}`]; + + return ( + + {heading} + {minVersion ? + + : null} + + ); +} +Heading.propTypes = { + headingLevel: PropTypes.number, + children: PropTypes.node.isRequired, + id: PropTypes.string, + as: PropTypes.any, + minVersion: PropTypes.string, +}; + +export function SubHeading({ canonicalReference, headingLevel, ...props }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + + return ( + + ); +} +SubHeading.propTypes = { + ...Heading.propTypes, + canonicalReference: PropTypes.string.isRequired, }; export function ApiDocHeading({ canonicalReference, headingLevel, - link = true, + signature = false, + since = false, + prefix = "", + suffix = "", + ...props }) { const MDX = useMDXComponents(); const getItem = useApiDocContext(); const item = getItem(canonicalReference); - const heading = + let heading = ( - item.kind === "MethodSignature" || - item.kind === "Function" || - item.kind === "Method" + signature && + (item.kind === "MethodSignature" || + item.kind === "Function" || + item.kind === "Method") ) ? - : item.displayName; + : {item.displayName}; + return ( - + - {link ? - - {heading} - - : heading} + {prefix} + {heading} + {suffix} - {item.file && ( - - - ({item.file}) - - - )} ); } ApiDocHeading.propTypes = { canonicalReference: PropTypes.string.isRequired, - headingLevel: PropTypes.number.isRequired, - link: PropTypes.bool, + headingLevel: PropTypes.number, + signature: PropTypes.bool, + since: PropTypes.bool, + prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), }; + +export function SectionHeading(props) { + return ( + + ); +} +SectionHeading.propTypes = Text.propTypes; diff --git a/docs/shared/ApiDoc/InterfaceDetails.js b/docs/shared/ApiDoc/InterfaceDetails.js index e4b439181c4..92c5ae7ae55 100644 --- a/docs/shared/ApiDoc/InterfaceDetails.js +++ b/docs/shared/ApiDoc/InterfaceDetails.js @@ -1,12 +1,21 @@ import PropTypes from "prop-types"; import React from "react"; -import { ApiDocHeading, DocBlock, PropertySignatureTable } from "."; +import { GridItem } from "@chakra-ui/react"; +import { + ApiDocHeading, + DocBlock, + PropertySignatureTable, + useApiDocContext, + SectionHeading, +} from "."; export function InterfaceDetails({ canonicalReference, headingLevel, link, customPropertyOrder, }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); return ( <> - + + + Properties + ); diff --git a/docs/shared/ApiDoc/ParameterTable.js b/docs/shared/ApiDoc/ParameterTable.js index 49f2a7abf9c..44bd51feada 100644 --- a/docs/shared/ApiDoc/ParameterTable.js +++ b/docs/shared/ApiDoc/ParameterTable.js @@ -2,10 +2,14 @@ import { useMDXComponents } from "@mdx-js/react"; import PropTypes from "prop-types"; import React from "react"; -import { GridItem, chakra } from "@chakra-ui/react"; -import { PropertySignatureTable, useApiDocContext } from "."; +import { GridItem, Text } from "@chakra-ui/react"; +import { + PropertySignatureTable, + SectionHeading, + getInterfaceReference, + useApiDocContext, +} from "."; import { ResponsiveGrid } from "./ResponsiveGrid"; -import { mdToReact } from "./mdToReact"; export function ParameterTable({ canonicalReference }) { const MDX = useMDXComponents(); @@ -16,45 +20,34 @@ export function ParameterTable({ canonicalReference }) { return ( <> - - - Parameters - - Name / Type Description {item.parameters.map((parameter) => { - const baseType = parameter.type.split("<")[0]; - const reference = getItem( - item.references?.find((r) => r.text === baseType) - ?.canonicalReference, - false + const interfaceReference = getInterfaceReference( + parameter.type, + item, + getItem ); - const interfaceReference = - reference?.kind === "Interface" ? reference : null; + const id = `${item.displayName.toLowerCase()}-parameters-${parameter.name.toLowerCase()}`; return ( - + - - {parameter.name} - {parameter.optional ? - (optional) - : null} - + + + {parameter.name} + {parameter.optional ? + (optional) + : null} + + {parameter.type} @@ -65,7 +58,9 @@ export function ParameterTable({ canonicalReference }) { lineHeight="base" borderBottom={interfaceReference ? "none" : undefined} > - {mdToReact(parameter.comment)} + {parameter.comment && ( + {parameter.comment} + )} {interfaceReference && (
@@ -75,8 +70,7 @@ export function ParameterTable({ canonicalReference }) {
)} @@ -90,4 +84,5 @@ export function ParameterTable({ canonicalReference }) { ParameterTable.propTypes = { canonicalReference: PropTypes.string.isRequired, + showHeaders: PropTypes.bool, }; diff --git a/docs/shared/ApiDoc/PropertyDetails.js b/docs/shared/ApiDoc/PropertyDetails.js new file mode 100644 index 00000000000..e8c0f28faae --- /dev/null +++ b/docs/shared/ApiDoc/PropertyDetails.js @@ -0,0 +1,27 @@ +import PropTypes from "prop-types"; +import React from "react"; +import { ApiDocHeading, DocBlock } from "."; + +export function PropertyDetails({ canonicalReference, headingLevel }) { + return ( + <> + + + + ); +} + +PropertyDetails.propTypes = { + canonicalReference: PropTypes.string.isRequired, + headingLevel: PropTypes.number.isRequired, +}; diff --git a/docs/shared/ApiDoc/PropertySignatureTable.js b/docs/shared/ApiDoc/PropertySignatureTable.js index b5d31feb18d..b25a9fd0810 100644 --- a/docs/shared/ApiDoc/PropertySignatureTable.js +++ b/docs/shared/ApiDoc/PropertySignatureTable.js @@ -2,56 +2,43 @@ import { useMDXComponents } from "@mdx-js/react"; import PropTypes from "prop-types"; import React, { useMemo } from "react"; -import { DocBlock, FunctionSignature, useApiDocContext } from "."; -import { GridItem, Text, chakra } from "@chakra-ui/react"; +import { + DocBlock, + FunctionSignature, + useApiDocContext, + ApiDocHeading, + SectionHeading, +} from "."; +import { GridItem, Text } from "@chakra-ui/react"; import { ResponsiveGrid } from "./ResponsiveGrid"; +import { groupItems } from "./sortWithCustomOrder"; export function PropertySignatureTable({ canonicalReference, prefix = "", - showHeaders = true, + showHeaders = false, display = "parent", customOrder = [], + idPrefix = "", }) { const MDX = useMDXComponents(); const getItem = useApiDocContext(); const item = getItem(canonicalReference); - const Wrapper = display === "parent" ? ResponsiveGrid : React.Fragment; - const sortedProperties = useMemo( - () => - item.properties.map(getItem).sort((a, b) => { - const aIndex = customOrder.indexOf(a.displayName); - const bIndex = customOrder.indexOf(b.displayName); - if (aIndex >= 0 && bIndex >= 0) { - return aIndex - bIndex; - } else if (aIndex >= 0) { - return -1; - } else if (bIndex >= 0) { - return 1; - } else { - return a.displayName.localeCompare(b.displayName); - } - }), + const Wrapper = display === "parent" ? ResponsiveGrid : React.Fragment; + const groupedProperties = useMemo( + () => groupItems(item.properties.map(getItem), customOrder), [item.properties, getItem, customOrder] ); + if (item.childrenIncomplete) { + console.warn( + "Warning: some properties might be missing from the table due to complex inheritance!", + item.childrenIncompleteDetails + ); + } return ( <> - {showHeaders ? - - - Properties - - - : null} {item.childrenIncomplete ?
@@ -67,46 +54,64 @@ export function PropertySignatureTable({ Description : null} - - {sortedProperties.map((property) => ( - - - - - - {prefix} - - {property.displayName} - - {property.optional ? - (optional) - : null} - - - {property.kind === "MethodSignature" ? - - : property.type} - - - - - - - ))} + {Object.entries(groupedProperties).map( + ([groupName, sortedProperties]) => ( + <> + {groupName ? + {groupName} + : null} + {sortedProperties.map((property) => ( + + + + {prefix} + + : null + } + suffix={property.optional ? (optional) : null} + link={!!idPrefix} + id={ + idPrefix ? + `${idPrefix}-${property.displayName.toLowerCase()}` + : undefined + } + /> + + {property.kind === "MethodSignature" ? + + : property.type} + + + + + + + ))} + + ) + )} ); @@ -118,4 +123,5 @@ PropertySignatureTable.propTypes = { showHeaders: PropTypes.bool, display: PropTypes.oneOf(["parent", "child"]), customOrder: PropTypes.arrayOf(PropTypes.string), + idPrefix: PropTypes.string, }; diff --git a/docs/shared/ApiDoc/ResponsiveGrid.js b/docs/shared/ApiDoc/ResponsiveGrid.js index 691a4afebcf..2f7b1b93931 100644 --- a/docs/shared/ApiDoc/ResponsiveGrid.js +++ b/docs/shared/ApiDoc/ResponsiveGrid.js @@ -57,7 +57,7 @@ export function ResponsiveGridStyles() { ); } -export function ResponsiveGrid({ children }) { +export function ResponsiveGrid({ children, columns = 2 }) { /* responsiveness not regarding screen width, but actual available space: if less than 350px, show only one column @@ -66,7 +66,11 @@ export function ResponsiveGrid({ children }) { return ( + + ({item.file}) + +
+ : null; +} +SourceLink.propTypes = { + canonicalReference: PropTypes.string.isRequired, +}; diff --git a/docs/shared/ApiDoc/Tuple.js b/docs/shared/ApiDoc/Tuple.js new file mode 100644 index 00000000000..3ab881151b3 --- /dev/null +++ b/docs/shared/ApiDoc/Tuple.js @@ -0,0 +1,68 @@ +import React from "react"; +import { useMDXComponents } from "@mdx-js/react"; +import { useApiDocContext, PropertySignatureTable } from "."; +import PropTypes from "prop-types"; + +export function ManualTuple({ elements = [], idPrefix = "" }) { + const MDX = useMDXComponents(); + const getItem = useApiDocContext(); + + return ( + + + + Name + Type + Description + + + + {elements.map( + ({ name, type, description, canonicalReference }, idx) => { + const item = getItem(canonicalReference); + const separatorStyle = item ? { borderBottom: 0 } : {}; + return ( + + + {name} + + {type} + + {description} + + {item ? + + +
+ Show/hide child attributes + +
+
+
+ : null} +
+ ); + } + )} +
+
+ ); +} +ManualTuple.propTypes = { + elements: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + description: PropTypes.oneOfType([PropTypes.node, PropTypes.string]) + .isRequired, + canonicalReference: PropTypes.string, + }) + ).isRequired, +}; diff --git a/docs/shared/ApiDoc/getInterfaceReference.js b/docs/shared/ApiDoc/getInterfaceReference.js new file mode 100644 index 00000000000..b9b9202ae92 --- /dev/null +++ b/docs/shared/ApiDoc/getInterfaceReference.js @@ -0,0 +1,8 @@ +export function getInterfaceReference(type, item, getItem) { + const baseType = type.replace(/\b(Partial|Omit|Promise) r.text === baseType)?.canonicalReference, + false + ); + return reference?.kind === "Interface" ? reference : null; +} diff --git a/docs/shared/ApiDoc/index.js b/docs/shared/ApiDoc/index.js index 66bfa413afa..4173bdebe62 100644 --- a/docs/shared/ApiDoc/index.js +++ b/docs/shared/ApiDoc/index.js @@ -1,14 +1,12 @@ export { useApiDocContext } from "./Context"; -export { - DocBlock, - Deprecated, - Example, - Remarks, - Since, - Summary, -} from "./DocBlock"; +export { DocBlock, Deprecated, Example, Remarks, Summary } from "./DocBlock"; export { PropertySignatureTable } from "./PropertySignatureTable"; -export { ApiDocHeading } from "./Heading"; +export { ApiDocHeading, SubHeading, SectionHeading } from "./Heading"; export { InterfaceDetails } from "./InterfaceDetails"; export { FunctionSignature, FunctionDetails } from "./Function"; export { ParameterTable } from "./ParameterTable"; +export { PropertyDetails } from "./PropertyDetails"; +export { EnumDetails } from "./EnumDetails"; +export { ManualTuple } from "./Tuple"; +export { getInterfaceReference } from "./getInterfaceReference"; +export { SourceLink } from "./SourceLink"; diff --git a/docs/shared/ApiDoc/mdToReact.js b/docs/shared/ApiDoc/mdToReact.js deleted file mode 100644 index 307ca38c7bf..00000000000 --- a/docs/shared/ApiDoc/mdToReact.js +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from "prop-types"; -import React from "react"; -import ReactMarkdown from "react-markdown"; -import { useMDXComponents } from "@mdx-js/react"; - -export function mdToReact(text) { - const sanitized = text - .replace(/\{@link (\w*)\}/g, "[$1](#$1)") - .replace(//g, ""); - return ; -} - -function RenderMd({ markdown }) { - return ( - {markdown} - ); -} -RenderMd.propTypes = { - markdown: PropTypes.string.isRequired, -}; diff --git a/docs/shared/ApiDoc/sortWithCustomOrder.js b/docs/shared/ApiDoc/sortWithCustomOrder.js new file mode 100644 index 00000000000..c2cd411e304 --- /dev/null +++ b/docs/shared/ApiDoc/sortWithCustomOrder.js @@ -0,0 +1,71 @@ +/** + * Sorts items by their `displayName` with a custom order: + * - items within the `customOrder` array will be sorted to the start, + * sorted by the order of the `customOrder` array + * - items not in the `customOrder` array will be sorted in lexicographical order after that + * - deprecated items will be sorted in lexicographical order to the end + */ +export function sortWithCustomOrder(customOrder = []) { + return (a, b) => { + let aIndex = customOrder.indexOf(a.displayName); + if (aIndex == -1) { + aIndex = + a.comment?.deprecated ? + Number.MAX_SAFE_INTEGER + : Number.MAX_SAFE_INTEGER - 1; + } + let bIndex = customOrder.indexOf(b.displayName); + if (bIndex == -1) { + bIndex = + b.comment?.deprecated ? + Number.MAX_SAFE_INTEGER + : Number.MAX_SAFE_INTEGER - 1; + } + if (aIndex === bIndex) { + return sortLocally(a.displayName, b.displayName); + } else { + return aIndex - bIndex; + } + }; +} + +function sortLocally(a, b) { + return a.localeCompare(b); +} + +/** + * + * @param {Array<{displayName: string, comment: { docGroup: string }}>} items + * @param {string[]} customOrder + */ +export function groupItems(items = [], customOrder = []) { + const customItems = []; + const groupedItems = []; + for (const item of items) { + if (customOrder.includes(item.displayName)) customItems.push(item); + else groupedItems.push(item); + } + customItems.sort(sortWithCustomOrder(customOrder)); + const groupNames = [ + ...new Set(groupedItems.map((item) => item.comment?.docGroup || "Other")), + ].sort(sortLocally); + const groups = Object.fromEntries(groupNames.map((name) => [name, []])); + for (const item of groupedItems) { + groups[item.comment?.docGroup || "Other"].push(item); + } + for (const group of Object.values(groups)) { + group.sort(sortWithCustomOrder([])); + } + const groupsWithoutPrefix = Object.fromEntries( + Object.entries(groups).map(([name, items]) => [ + name.replace(/^\s*\d*\.\s*/, ""), + items, + ]) + ); + return customItems.length === 0 ? + groupsWithoutPrefix + : { + "": customItems, + ...groupsWithoutPrefix, + }; +} diff --git a/docs/shared/apollo-provider.mdx b/docs/shared/apollo-provider.mdx deleted file mode 100644 index 8b137891791..00000000000 --- a/docs/shared/apollo-provider.mdx +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/shared/document-transform-options.mdx b/docs/shared/document-transform-options.mdx deleted file mode 100644 index 3675c19cfdf..00000000000 --- a/docs/shared/document-transform-options.mdx +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -###### `getCacheKey` - -`(document: DocumentNode) => any[] | undefined` - - -Defines a custom cache key for a GraphQL document that will determine whether to re-run the document transform when given the same input GraphQL document. Returns an array that defines the cache key. Return `undefined` to disable caching for that GraphQL document. - -> **Note:** The items in the array may be any type, but also need to be referentially stable to guarantee a stable cache key. - -The default implementation of this function returns the `document` as the cache key. -
- -###### `cache` - -`boolean` - - -Determines whether to cache the transformed GraphQL document. Caching can speed up repeated calls to the document transform for the same input document. Set to `false` to completely disable caching for the document transform. When disabled, this option takes precedence over the [`getCacheKey`](#getcachekey) option. - -The default value is `true`. -
- diff --git a/docs/shared/mutation-options.mdx b/docs/shared/mutation-options.mdx deleted file mode 100644 index 4357464ae9b..00000000000 --- a/docs/shared/mutation-options.mdx +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -**Operation options** - -
- -###### `mutation` - -`DocumentNode` - - -A GraphQL query string parsed into an AST with the `gql` template literal. - -**Optional** for the `useMutation` hook, because the mutation can also be provided as the first parameter to the hook. - -**Required** for the `Mutation` component. -
- -###### `variables` - -`{ [key: string]: any }` - - -An object containing all of the GraphQL variables your mutation requires to execute. - -Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. - -
- -###### `errorPolicy` - -`ErrorPolicy` - - -Specifies how the mutation handles a response that returns both GraphQL errors and partial results. - -For details, see [GraphQL error policies](/react/data/error-handling/#graphql-error-policies). - -The default value is `none`, meaning that the mutation result includes error details but _not_ partial results. - -
- -###### `onCompleted` - -`(data?: TData, clientOptions?: BaseMutationOptions) => void` - - -A callback function that's called when your mutation successfully completes with zero errors (or if `errorPolicy` is `ignore` and partial data is returned). - -This function is passed the mutation's result `data` and any options passed to the mutation. - -
- -###### `onError` - -`(error: ApolloError, clientOptions?: BaseMutationOptions) => void` - - -A callback function that's called when the mutation encounters one or more errors (unless `errorPolicy` is `ignore`). - -This function is passed an [`ApolloError`](https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/errors/index.ts#L36-L39) object that contains either a `networkError` object or a `graphQLErrors` array, depending on the error(s) that occurred, as well as any options passed the mutation. - -
- -###### `onQueryUpdated` - -`(observableQuery: ObservableQuery, diff: Cache.DiffResult, lastDiff: Cache.DiffResult | undefined) => boolean | TResult` - - - -Optional callback for intercepting queries whose cache data has been updated by the mutation, as well as any queries specified in the [`refetchQueries: [...]`](#refetchQueries) list passed to `client.mutate`. - -Returning a `Promise` from `onQueryUpdated` will cause the final mutation `Promise` to await the returned `Promise`. Returning `false` causes the query to be ignored. - -
- -###### `refetchQueries` - -`Array | ((mutationResult: FetchResult) => Array)` - - -An array (or a function that _returns_ an array) that specifies which queries you want to refetch after the mutation occurs. - -Each array value can be either: - -* An object containing the `query` to execute, along with any `variables` -* A string indicating the operation name of the query to refetch - -
- -###### `awaitRefetchQueries` - -`boolean` - - -If `true`, makes sure all queries included in `refetchQueries` are completed before the mutation is considered complete. - -The default value is `false` (queries are refetched asynchronously). - -
- -###### `ignoreResults` - -`boolean` - - -If `true`, the mutation's `data` property is not updated with the mutation's result. - -The default value is `false`. - -
- -**Networking options** - -
- -###### `notifyOnNetworkStatusChange` - -`boolean` - - -If `true`, the in-progress mutation's associated component re-renders whenever the network status changes or a network error occurs. - -The default value is `false`. - -
- -###### `client` - -`ApolloClient` - - -The instance of `ApolloClient` to use to execute the mutation. - -By default, the instance that's passed down via context is used, but you can provide a different instance here. - -
- -###### `context` - -`Record` - - -If you're using [Apollo Link](/react/api/link/introduction/), this object is the initial value of the `context` object that's passed along your link chain. - -
- -**Caching options** - -
- -###### `update` - -`(cache: ApolloCache, mutationResult: FetchResult) => void` - - -A function used to update the Apollo Client cache after the mutation completes. - -For more information, see [Updating the cache after a mutation](/react/data/mutations#updating-the-cache-after-a-mutation). - -
- -###### `optimisticResponse` - -`TData | (vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier }) => TData` - - -By providing either an object or a callback function that, when invoked after a mutation, allows you to return optimistic data and optionally skip updates via the `IGNORE` sentinel object, Apollo Client caches this temporary (and potentially incorrect) response until the mutation completes, enabling more responsive UI updates. - -For more information, see [Optimistic mutation results](/react/performance/optimistic-ui/). - -
- -###### `fetchPolicy` - -`MutationFetchPolicy` - - -Provide `no-cache` if the mutation's result should _not_ be written to the Apollo Client cache. - -The default value is `network-only` (which means the result _is_ written to the cache). - -Unlike queries, mutations _do not_ support [fetch policies](/react/data/queries/#setting-a-fetch-policy) besides `network-only` and `no-cache`. - -
diff --git a/docs/shared/mutation-result.mdx b/docs/shared/mutation-result.mdx deleted file mode 100644 index 21e4e858137..00000000000 --- a/docs/shared/mutation-result.mdx +++ /dev/null @@ -1,145 +0,0 @@ -**Mutate function:** - - - - - - - - - - - - - - - -
Name /
Type
Description
- -###### `mutate` - -`(options?: MutationOptions) => Promise` - - -A function to trigger the mutation from your UI. You can optionally pass this function any of the following options: - -* `awaitRefetchQueries` -* `context` -* `fetchPolicy` -* `onCompleted` -* `onError` -* `optimisticResponse` -* `refetchQueries` -* `onQueryUpdated` -* `update` -* `variables` -* `client` - -Any option you pass here overrides any existing value for that option that you passed to `useMutation`. - -The mutate function returns a promise that fulfills with your mutation result. -
- -**Mutation result:** - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -###### `data` - -`TData` - - -The data returned from your mutation. Can be `undefined` if `ignoreResults` is `true`. -
- -###### `loading` - -`boolean` - - -If `true`, the mutation is currently in flight. -
- -###### `error` - -`ApolloError` - - -If the mutation produces one or more errors, this object contains either an array of `graphQLErrors` or a single `networkError`. Otherwise, this value is `undefined`. - -For more information, see [Handling operation errors](/react/data/error-handling/). - -
- -###### `called` - -`boolean` - - -If `true`, the mutation's mutate function has been called. - -
- -###### `client` - -`ApolloClient` - - -The instance of Apollo Client that executed the mutation. - -Can be useful for manually executing followup operations or writing data to the cache. - -
- -###### `reset` - -`() => void` - - -A function that you can call to reset the mutation's result to its initial, uncalled state. - -
diff --git a/docs/shared/query-options.mdx b/docs/shared/query-options.mdx deleted file mode 100644 index 2a265ada2da..00000000000 --- a/docs/shared/query-options.mdx +++ /dev/null @@ -1,304 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -**Operation options** - -
- -###### `query` - -`DocumentNode` - - -A GraphQL query string parsed into an AST with the `gql` template literal. - -**Optional** for the `useQuery` hook, because the query can be provided as the first parameter to the hook. **Required** for the `Query` component. -
- -###### `variables` - -`{ [key: string]: any }` - - -An object containing all of the GraphQL variables your query requires to execute. - -Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. - -
- -###### `errorPolicy` - -`ErrorPolicy` - - -Specifies how the query handles a response that returns both GraphQL errors and partial results. - -For details, see [GraphQL error policies](/react/data/error-handling/#graphql-error-policies). - -The default value is `none`, meaning that the query result includes error details but _not_ partial results. - -
- -###### `onCompleted` - -`(data: TData | {}) => void` - - -A callback function that's called when your query successfully completes with zero errors (or if `errorPolicy` is `ignore` and partial data is returned). - -This function is passed the query's result `data`. - -
- -###### `onError` - -`(error: ApolloError) => void` - - -A callback function that's called when the query encounters one or more errors (unless `errorPolicy` is `ignore`). - -This function is passed an [`ApolloError`](https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/errors/index.ts#L36-L39) object that contains either a `networkError` object or a `graphQLErrors` array, depending on the error(s) that occurred. - -
- -###### `skip` - -`boolean` - - -If `true`, the query is _not_ executed. **Not available with `useLazyQuery`.** - -This property is part of Apollo Client's React integration, and it is _not_ available in the [core `ApolloClient` API](/react/api/core/ApolloClient/). - -The default value is `false`. - -
- -**Networking options** - -
- -###### `pollInterval` - -`number` - - -Specifies the interval (in milliseconds) at which the query polls for updated results. - -The default value is `0` (no polling). - -
- -###### `notifyOnNetworkStatusChange` - -`boolean` - - -If `true`, the in-progress query's associated component re-renders whenever the network status changes or a network error occurs. - -The default value is `false`. - -
- -###### `context` - -`Record` - - -If you're using [Apollo Link](/react/api/link/introduction/), this object is the initial value of the `context` object that's passed along your link chain. - -
- -###### `ssr` - -`boolean` - - -Pass `false` to skip executing the query during [server-side rendering](/react/performance/server-side-rendering/). - -
- -###### `client` - -`ApolloClient` - - -The instance of `ApolloClient` to use to execute the query. - -By default, the instance that's passed down via context is used, but you can provide a different instance here. - -
- -**Caching options** - -
- -###### `fetchPolicy` - -`FetchPolicy` - - -Specifies how the query interacts with the Apollo Client cache during execution (for example, whether it checks the cache for results before sending a request to the server). - -For details, see [Setting a fetch policy](/react/data/queries/#setting-a-fetch-policy). - -The default value is `cache-first`. - -
- -###### `nextFetchPolicy` - -`FetchPolicy` - - -Specifies the [`fetchPolicy`](#fetchpolicy) to use for all executions of this query _after_ this execution. - -For example, you can use this to switch back to a `cache-first` fetch policy after using `cache-and-network` or `network-only` for a single execution. - -
- -###### `returnPartialData` - -`boolean` - - -If `true`, the query can return _partial_ results from the cache if the cache doesn't contain results for _all_ queried fields. - -The default value is `false`. - -
- -**Deprecated options** - -
- -###### `partialRefetch` - -`boolean` - - -**Deprecated.** If `true`, causes a query `refetch` if the query result is detected as partial. Setting this option is unnecessary in Apollo Client 3, thanks to a more consistent application of fetch policies. It might be removed in a future release. - -The default value is `false`. - -
diff --git a/docs/shared/query-result.mdx b/docs/shared/query-result.mdx deleted file mode 100644 index aed5e2f478f..00000000000 --- a/docs/shared/query-result.mdx +++ /dev/null @@ -1,275 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -**Operation data** - -
- -###### `data` - -`TData` - - -An object containing the result of your GraphQL query after it completes. - -This value might be `undefined` if a query results in one or more errors (depending on the query's `errorPolicy`). - -
- -###### `previousData` - -`TData` - - -An object containing the result from the most recent _previous_ execution of this query. - -This value is `undefined` if this is the query's first execution. - -
- -###### `error` - -`ApolloError` - - -If the query produces one or more errors, this object contains either an array of `graphQLErrors` or a single `networkError`. Otherwise, this value is `undefined`. - -For more information, see [Handling operation errors](/react/data/error-handling/). - -
- -###### `variables` - -`{ [key: string]: any }` - - -An object containing the variables that were provided for the query. - -
- -**Network info** - -
- -###### `loading` - -`boolean` - - -If `true`, the query is still in flight and results have not yet been returned. - -
- -###### `networkStatus` - -`NetworkStatus` - - -A number indicating the current network state of the query's associated request. [See possible values.](https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/core/networkStatus.ts#L4) - -Used in conjunction with the [`notifyOnNetworkStatusChange`](#notifyonnetworkstatuschange) option. - -
- -###### `client` - -`ApolloClient` - - -The instance of Apollo Client that executed the query. - -Can be useful for manually executing followup queries or writing data to the cache. - -
- -###### `called` - -`boolean` - - -If `true`, the associated lazy query has been executed. - -This field is only present on the result object returned by [`useLazyQuery`](/react/data/queries/#executing-queries-manually). - -
- -**Helper functions** - -
- -###### `refetch` - -`(variables?: Partial) => Promise` - - -A function that enables you to re-execute the query, optionally passing in new `variables`. - -To guarantee that the refetch performs a network request, its `fetchPolicy` is set to `network-only` (unless the original query's `fetchPolicy` is `no-cache` or `cache-and-network`, which also guarantee a network request). - -See also [Refetching](/react/data/queries/#refetching). - -
- -###### `fetchMore` - -`({ query?: DocumentNode, variables?: TVariables, updateQuery: Function}) => Promise` - - -A function that helps you fetch the next set of results for a [paginated list field](/react/pagination/core-api/). - -
- -###### `startPolling` - -`(interval: number) => void` - - -A function that instructs the query to begin re-executing at a specified interval (in milliseconds). - -
- -###### `stopPolling` - -`() => void` - - -A function that instructs the query to stop polling after a previous call to `startPolling`. - -
- -###### `subscribeToMore` - -`(options: { document: DocumentNode, variables?: TVariables, updateQuery?: Function, onError?: Function}) => () => void` - - -A function that enables you to execute a [subscription](/react/data/subscriptions/), usually to subscribe to specific fields that were included in the query. - -This function returns _another_ function that you can call to terminate the subscription. - -
- -###### `updateQuery` - -`(mapFn: (previousResult: TData, options: { variables: TVariables }) => TData) => void` - - -A function that enables you to update the query's cached result without executing a followup GraphQL operation. -See [using updateQuery and updateFragment](/react/caching/cache-interaction/#using-updatequery-and-updatefragment) for additional information. -
diff --git a/docs/shared/subscription-options.mdx b/docs/shared/subscription-options.mdx deleted file mode 100644 index 00148fa2fce..00000000000 --- a/docs/shared/subscription-options.mdx +++ /dev/null @@ -1,14 +0,0 @@ -| Option | Type | Description | -| - | - | - | -| `subscription` | DocumentNode | A GraphQL subscription document parsed into an AST by `graphql-tag`. **Optional** for the `useSubscription` Hook since the subscription can be passed in as the first parameter to the Hook. **Required** for the `Subscription` component. | -| `variables` | { [key: string]: any } | An object containing all of the variables your subscription needs to execute | -| `shouldResubscribe` | boolean | Determines if your subscription should be unsubscribed and subscribed again when an input to the hook (such as `subscription` or `variables`) changes. | -| `skip` | boolean | Determines if the current subscription should be skipped. Useful if, for example, variables depend on previous queries and are not ready yet. | -| `onSubscriptionData` | **Deprecated.** (options: OnSubscriptionDataOptions<TData>) => any | Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives data. The callback `options` object param consists of the current Apollo Client instance in `client`, and the received subscription data in `subscriptionData`. | -| `onData` | (options: OnDataOptions<TData>) => any | Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives data. The callback `options` object param consists of the current Apollo Client instance in `client`, and the received subscription data in `data`. | -| `onError` | (error: ApolloError) => void | Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives an error. | -| `onSubscriptionComplete` | **Deprecated.** () => void | Allows the registration of a callback function that will be triggered when the `useSubscription` Hook / `Subscription` component completes the subscription. | -| `onComplete` | () => void | Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component completes the subscription. | -| `fetchPolicy` | FetchPolicy | How you want your component to interact with the Apollo cache. For details, see [Setting a fetch policy](/react/data/queries/#setting-a-fetch-policy). | -| `context` | Record<string, any> | Shared context between your component and your network interface (Apollo Link). | -| `client` | ApolloClient | An `ApolloClient` instance. By default `useSubscription` / `Subscription` uses the client passed down via context, but a different client can be passed in. | diff --git a/docs/shared/subscription-result.mdx b/docs/shared/subscription-result.mdx deleted file mode 100644 index be7d5295613..00000000000 --- a/docs/shared/subscription-result.mdx +++ /dev/null @@ -1,5 +0,0 @@ -| Property | Type | Description | -| - | - | - | -| `data` | TData | An object containing the result of your GraphQL subscription. Defaults to an empty object. | -| `loading` | boolean | A boolean that indicates whether any initial data has been returned | -| `error` | ApolloError | A runtime error with `graphQLErrors` and `networkError` properties | diff --git a/docs/shared/useSuspenseQuery-options.mdx b/docs/shared/useSuspenseQuery-options.mdx deleted file mode 100644 index 38d6189d21f..00000000000 --- a/docs/shared/useSuspenseQuery-options.mdx +++ /dev/null @@ -1,217 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -**Operation options** - -
- -###### `variables` - -`{ [key: string]: any }` - - -An object containing all of the GraphQL variables your query requires to execute. - -Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. - -
- -###### `errorPolicy` - -`ErrorPolicy` - - -Specifies how the query handles a response that returns both GraphQL errors and partial results. - -For details, see [GraphQL error policies](/react/data/error-handling/#graphql-error-policies). - -The default value is `none`, which causes the hook to throw the error. -
- -**Networking options** - -
- -###### `context` - -`Record` - - -If you're using [Apollo Link](/react/api/link/introduction/), this object is the initial value of the `context` object that's passed along your link chain. - -
- -###### `canonizeResults` - -`Boolean` - - -> **⚠️ Deprecated**: -> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. -> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. - -If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. - -The default value is `false`. - -
- -###### `client` - -`ApolloClient` - - -The instance of `ApolloClient` to use to execute the query. - -By default, the instance that's passed down via context is used, but you can provide a different instance here. - -
- -###### `queryKey` - -`string | number | any[]` - - -A unique identifier for the query. Each item in the array must be a stable identifier to prevent infinite fetches. - -This is useful when using the same query and variables combination in more than one component, otherwise the components may clobber each other. This can also be used to force the query to re-evaluate fresh. - -
- -**Caching options** - -
- -###### `fetchPolicy` - -`SuspenseQueryHookFetchPolicy` - - -Specifies how the query interacts with the Apollo Client cache during execution (for example, whether it checks the cache for results before sending a request to the server). - -For details, see [Setting a fetch -policy](/react/data/queries/#setting-a-fetch-policy). This hook only supports -the `cache-first`, `network-only`, `no-cache`, and `cache-and-network` fetch -policies. - -The default value is `cache-first`. - -
- -###### `returnPartialData` - -`boolean` - - -If `true`, the query can return _partial_ results from the cache if the cache doesn't contain results for _all_ queried fields. - -The default value is `false`. - -
- -###### `refetchWritePolicy` - -`"merge" | "overwrite"` - - -Watched queries must opt into overwriting existing data on refetch, by passing `refetchWritePolicy: "overwrite"` in their `WatchQueryOptions`. - -The default value is `"overwrite"`. - -
- -###### `skip` (deprecated) - -`boolean` - - -If `true`, the query is not executed. The default value is `false`. - -This option is deprecated and only supported to ease the migration from `useQuery`. It will be removed in a future release. -Please use [`skipToken`](/react/api/react/hooks#skiptoken`) instead of the `skip` option as it is more type-safe. - -
diff --git a/docs/source/api/core/ObservableQuery.mdx b/docs/source/api/core/ObservableQuery.mdx index 90d4450dfe6..806091f8b28 100644 --- a/docs/source/api/core/ObservableQuery.mdx +++ b/docs/source/api/core/ObservableQuery.mdx @@ -1,26 +1,30 @@ --- title: ObservableQuery description: API reference +api_doc: + - "@apollo/client!ObservableQuery:class" + - "@apollo/client!ApolloQueryResult:interface" + - "@apollo/client!NetworkStatus:enum" --- +import { InterfaceDetails, FunctionDetails, PropertyDetails, EnumDetails } from '../../../shared/ApiDoc'; + ## `ObservableQuery` functions `ApolloClient` Observables extend the Observables implementation provided by [`zen-observable`](https://github.com/zenparsing/zen-observable). Refer to the `zen-observable` documentation for additional context and API options. - - - - - - - - - - - + + + + + + + + + + -## Types - - - +## Types + + diff --git a/docs/source/api/react/components.mdx b/docs/source/api/react/components.mdx index 629d1475c8d..7322b162234 100644 --- a/docs/source/api/react/components.mdx +++ b/docs/source/api/react/components.mdx @@ -1,14 +1,16 @@ --- title: Components description: Deprecated React Apollo render prop component API +api_doc: + - "@apollo/client!QueryFunctionOptions:interface" + - "@apollo/client!QueryResult:interface" + - "@apollo/client!MutationFunctionOptions:interface" + - "@apollo/client!MutationResult:interface" + - "@apollo/client!SubscriptionComponentOptions:interface" + - "@apollo/client!SubscriptionResult:interface" --- -import QueryOptions3 from '../../../shared/query-options.mdx'; -import QueryResult3 from '../../../shared/query-result.mdx'; -import MutationOptions3 from '../../../shared/mutation-options.mdx'; -import MutationResult3 from '../../../shared/mutation-result.mdx'; -import SubscriptionOptions3 from '../../../shared/subscription-options.mdx'; -import SubscriptionResult3 from '../../../shared/subscription-result.mdx'; +import { PropertySignatureTable } from '../../../shared/ApiDoc'; > **Note:** Official support for React Apollo render prop components ended in March 2020. This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. @@ -28,25 +30,25 @@ You then import the library's symbols from `@apollo/client/react/components`. The `Query` component accepts the following props. `query` is **required**. - + ### Render prop function The render prop function that you pass to the `children` prop of `Query` is called with an object (`QueryResult`) that has the following properties. This object contains your query result, plus some helpful functions for refetching, dynamic polling, and pagination. - + ## `Mutation` The Mutation component accepts the following props. Only `mutation` is **required**. - + ### Render prop function The render prop function that you pass to the `children` prop of `Mutation` is called with the `mutate` function and an object with the mutation result. The `mutate` function is how you trigger the mutation from your UI. The object contains your mutation result, plus loading and error state. - + ## `Subscription` @@ -54,10 +56,10 @@ The render prop function that you pass to the `children` prop of `Mutation` is c The Subscription component accepts the following props. Only `subscription` is **required**. - + ### Render prop function -The render prop function that you pass to the `children` prop of `Subscription` is called with an object that has the following properties. + diff --git a/docs/source/api/react/hooks.mdx b/docs/source/api/react/hooks.mdx index 9a729b1c3a6..c6e3249253b 100644 --- a/docs/source/api/react/hooks.mdx +++ b/docs/source/api/react/hooks.mdx @@ -1,25 +1,24 @@ --- title: Hooks description: Apollo Client react hooks API reference +minVersion: 3.0.0 +api_doc: + - "@apollo/client!SuspenseQueryHookOptions:interface" + - "@apollo/client!useQuery:function(1)" + - "@apollo/client!useLazyQuery:function(1)" + - "@apollo/client!useMutation:function(1)" + - "@apollo/client!useSubscription:function(1)" + - "@apollo/client!useApolloClient:function(1)" + - "@apollo/client!useReactiveVar:function(1)" --- -import QueryOptions3 from '../../../shared/query-options.mdx'; -import QueryResult3 from '../../../shared/query-result.mdx'; -import MutationOptions3 from '../../../shared/mutation-options.mdx'; -import MutationResult3 from '../../../shared/mutation-result.mdx'; -import SubscriptionOptions3 from '../../../shared/subscription-options.mdx'; -import SubscriptionResult3 from '../../../shared/subscription-result.mdx'; import UseFragmentOptions from '../../../shared/useFragment-options.mdx'; import UseFragmentResult from '../../../shared/useFragment-result.mdx'; -import UseSuspenseQueryOptions from '../../../shared/useSuspenseQuery-options.mdx'; import UseBackgroundQueryOptions from '../../../shared/useBackgroundQuery-options.mdx'; import UseSuspenseQueryResult from '../../../shared/useSuspenseQuery-result.mdx'; import UseBackgroundQueryResult from '../../../shared/useBackgroundQuery-result.mdx'; import UseReadQueryResult from '../../../shared/useReadQuery-result.mdx'; - -## Installation - -Apollo Client >= 3 includes React hooks functionality out of the box. You don't need to install any additional packages. +import { FunctionDetails, PropertySignatureTable, ManualTuple, InterfaceDetails } from '../../../shared/ApiDoc'; ## The `ApolloProvider` component @@ -69,350 +68,93 @@ function WithApolloClient() { } ``` -## `useQuery` - -### Example - -```jsx -import { gql, useQuery } from '@apollo/client'; - -const GET_GREETING = gql` - query GetGreeting($language: String!) { - greeting(language: $language) { - message - } - } -`; - -function Hello() { - const { loading, error, data } = useQuery(GET_GREETING, { - variables: { language: 'english' }, - }); - if (loading) return

Loading ...

; - return

Hello {data.greeting.message}!

; -} -``` - -> Refer to the [Queries](../../data/queries/) section for a more in-depth overview of `useQuery`. - -### Signature - -```ts -function useQuery( - query: DocumentNode, - options?: QueryHookOptions, -): QueryResult {} -``` - -### Params - -#### `query` - -| Param | Type | Description | -| ------- | ------------ | ------------------------------------------------------------- | -| `query` | DocumentNode | A GraphQL query document parsed into an AST by `gql`. | - -#### `options` - - - -### Result - - - -## `useLazyQuery` - -### Example - -```jsx -import { gql, useLazyQuery } from "@apollo/client"; - -const GET_GREETING = gql` - query GetGreeting($language: String!) { - greeting(language: $language) { - message - } - } -`; - -function Hello() { - const [loadGreeting, { called, loading, data }] = useLazyQuery( - GET_GREETING, - { variables: { language: "english" } } - ); - if (called && loading) return

Loading ...

- if (!called) { - return - } - return

Hello {data.greeting.message}!

; -} -``` - -> Refer to the [Queries](../../data/queries/) section for a more in-depth overview of `useLazyQuery`. - -### Signature - -```ts -function useLazyQuery( - query: DocumentNode, - options?: LazyQueryHookOptions, -): [ - (options?: LazyQueryHookOptions) => Promise>, - LazyQueryResult -] {} -``` - -### Params - -#### `query` - -| Param | Type | Description | -| ------- | ------------ | ------------------------------------------------------------- | -| `query` | DocumentNode | A GraphQL query document parsed into an AST by `gql`. | - -#### `options` - - - -### Result tuple - -**Execute function (first tuple item)** - -| Param | Type | Description | -| ---------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| Execute function | `(options?: LazyQueryHookOptions) => Promise>` | Function that can be triggered to execute the suspended query. After being called, `useLazyQuery` behaves just like `useQuery`. The `useLazyQuery` function returns a promise that fulfills with a query result when the query succeeds or fails. | + -**`LazyQueryResult` object (second tuple item)** + +

+[execute: LazyQueryExecFunction<TData, TVariables>, result: QueryResult<TData, TVariables>]
+
- +A tuple of two values: -## `useMutation` - -### Example - -```jsx -import { gql, useMutation } from '@apollo/client'; - -const ADD_TODO = gql` - mutation AddTodo($type: String!) { - addTodo(type: $type) { - id - type - } +) => Promise>", + description: "Function that can be triggered to execute the suspended query. After being called, `useLazyQuery` behaves just like `useQuery`. The `useLazyQuery` function returns a promise that fulfills with a query result when the query succeeds or fails." + }, + { + name: "result", + type: "QueryResult", + description: "The result of the query. See the `useQuery` hook for more details.", + canonicalReference: "@apollo/client!QueryResult:interface" } -`; - -function AddTodo() { - let input; - const [addTodo, { data }] = useMutation(ADD_TODO); - - return ( +]}/> +} +/> + + -
{ - e.preventDefault(); - addTodo({ variables: { type: input.value } }); - input.value = ''; - }} - > - { - input = node; - }} - /> - -
+
+        
+        {`[
+  mutate: (options?: MutationFunctionOptions) => Promise>,
+  result: MutationResult
+]`}
+        
+      
+ A tuple of two values: + + ) => Promise>`, + description:
+A function to trigger the mutation from your UI. You can optionally pass this function any of the following options: + +
    +
  • awaitRefetchQueries
  • +
  • context
  • +
  • fetchPolicy
  • +
  • onCompleted
  • +
  • onError
  • +
  • optimisticResponse
  • +
  • refetchQueries
  • +
  • onQueryUpdated
  • +
  • update
  • +
  • variables
  • +
  • client
  • +
+ +Any option you pass here overrides any existing value for that option that you passed to useMutation. + +The mutate function returns a promise that fulfills with your mutation result. +
, + }, + { + name: "result", + type: "MutationResult", + description: "The result of the mutation.", + canonicalReference: "@apollo/client!MutationResult:interface", + }, + ]} + /> - ); -} -``` - -> Refer to the [Mutations](../../data/mutations/) section for a more in-depth overview of `useMutation`. - -### Signature - -```ts -function useMutation( - mutation: DocumentNode, - options?: MutationHookOptions, -): MutationTuple {} -``` - -### Params - -#### `mutation` - -| Param | Type | Description | -| ---------- | ------------ | ---------------------------------------------------------------- | -| `mutation` | DocumentNode | A GraphQL mutation document parsed into an AST by `gql`. | - -#### `options` - - - -### `MutationTuple` result tuple - - - -## `useSubscription` - -### Example - -```jsx -const COMMENTS_SUBSCRIPTION = gql` - subscription OnCommentAdded($repoFullName: String!) { - commentAdded(repoFullName: $repoFullName) { - id - content - } } -`; - -function DontReadTheComments({ repoFullName }) { - const { - data: { commentAdded }, - loading, - } = useSubscription(COMMENTS_SUBSCRIPTION, { variables: { repoFullName } }); - return

New comment: {!loading && commentAdded.content}

; -} -``` - -> Refer to the [Subscriptions](../../data/subscriptions/) section for a more in-depth overview of `useSubscription`. - -#### Subscriptions and React 18 Automatic Batching - -With React 18's [automatic batching](https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching), multiple state updates may be grouped into a single re-render for better performance. - -If your subscription API sends multiple messages at the same time or in very fast succession (within fractions of a millisecond), it is likely that only the last message received in that narrow time frame will result in a re-render. - -Consider the following component: - -```jsx -export function Subscriptions() { - const { data, error, loading } = useSubscription(query); - const [accumulatedData, setAccumulatedData] = useState([]); - - useEffect(() => { - setAccumulatedData((prev) => [...prev, data]); - }, [data]); - - return ( - <> - {loading &&

Loading...

} - {JSON.stringify(accumulatedData, undefined, 2)} - - ); -} -``` - -If your subscription back-end emits two messages with the same timestamp, only the last message received by Apollo Client will be rendered. This is because React 18 will batch these two state updates into a single re-render. - -Since the component above is using `useEffect` to push `data` into a piece of local state on each `Subscriptions` re-render, the first message will never be added to the `accumulatedData` array since its render was skipped. - -Instead of using `useEffect` here, we can re-write this component to use the `onData` callback function accepted in `useSubscription`'s `options` object: - -```jsx -export function Subscriptions() { - const [accumulatedData, setAccumulatedData] = useState([]); - const { data, error, loading } = useSubscription( - query, - { - onData({ data }) { - setAccumulatedData((prev) => [...prev, data]) - } - } - ); - - return ( - <> - {loading &&

Loading...

} - {JSON.stringify(accumulatedData, undefined, 2)} - - ); -} -``` +/> -> ⚠️ **Note:** The `useSubscription` option `onData` is available in Apollo Client >= 3.7. In previous versions, the equivalent option is named `onSubscriptionData`. + -Now, the first message will be added to the `accumulatedData` array since `onData` is called _before_ the component re-renders. React 18 automatic batching is still in effect and results in a single re-render, but with `onData` we can guarantee each message received after the component mounts is added to `accumulatedData`. + -### Signature - -```ts -function useSubscription( - subscription: DocumentNode, - options?: SubscriptionHookOptions, -): { - variables: TVariables; - loading: boolean; - data?: TData; - error?: ApolloError; -} {} -``` - -### Params - -#### `subscription` - -| Param | Type | Description | -| -------------- | ------------ | -------------------------------------------------------------------- | -| `subscription` | DocumentNode | A GraphQL subscription document parsed into an AST by `gql`. | - -#### `options` - - - -### Result - - - -## `useApolloClient` - -### Example - -```jsx -import { useApolloClient } from '@apollo/client'; - -function SomeComponent() { - const client = useApolloClient(); - // `client` is now set to the `ApolloClient` instance being used by the - // application (that was configured using something like `ApolloProvider`) -} -``` - -### Signature - -```ts -function useApolloClient(): ApolloClient {} -``` - -### Result - -| Param | Type | Description | -| ---------------------- | -------------------------- | ---------------------------------------------------------- | -| Apollo Client instance | ApolloClient<object> | The `ApolloClient` instance being used by the application. | - - -## `useReactiveVar` - -Reads the value of a [reactive variable](../../local-state/reactive-variables/) and re-renders the containing component whenever that variable's value changes. This enables a reactive variable to trigger changes _without_ relying on the `useQuery` hook. - -### Example - -```jsx -import { makeVar, useReactiveVar } from "@apollo/client"; -export const cartItemsVar = makeVar([]); - -export function Cart() { - const cartItems = useReactiveVar(cartItemsVar); - // ... -``` - -### Signature - -```tsx -function useReactiveVar(rv: ReactiveVar): T {} -``` + @@ -520,7 +262,11 @@ function useSuspenseQuery( Instead of passing a `SuspenseQueryHookOptions` object into the hook, you can also pass a [`skipToken`](#skiptoken) to prevent the `useSuspenseQuery` hook from executing the query or suspending. - + ### Result diff --git a/docs/source/data/document-transforms.mdx b/docs/source/data/document-transforms.mdx index d9acc5f8a54..96510c4e27e 100644 --- a/docs/source/data/document-transforms.mdx +++ b/docs/source/data/document-transforms.mdx @@ -2,9 +2,10 @@ title: Document transforms description: Make custom modifications to your GraphQL documents minVersion: 3.8.0 +api_doc: + - "@apollo/client!~DocumentTransformOptions:interface" --- - -import DocumentTransformOptions from '../../shared/document-transform-options.mdx'; +import { InterfaceDetails } from '../../shared/ApiDoc'; > This article assumes you're familiar with the [anatomy of a GraphQL query](https://www.apollographql.com/blog/graphql/basics/the-anatomy-of-a-graphql-query/) and the concept of an [abstract syntax tree (AST)](https://en.wikipedia.org/wiki/Abstract_syntax_tree). To explore a GraphQL AST, visit [AST Explorer](https://astexplorer.net/). @@ -29,7 +30,7 @@ const client = new ApolloClient({ }); ``` -### Lifecycle +### Lifecycle Apollo Client runs document transforms before every GraphQL request for all operations. This extends to any API that performs a network request, such as the [`useQuery`](/react/api/react/hooks#usequery) hook or the [`refetch`](/react/api/core/ObservableQuery#refetch) function on [`ObservableQuery`](/react/api/core/ObservableQuery). @@ -111,11 +112,11 @@ const transformedDocument = visit(document, { ``` > Returning `undefined` from our `Field` visitor tells the `visit` function to leave the node unchanged. -Now that we've determined we are working with the `currentUser` field, we need to figure out if our `id` field is already part of the `currentUser` field's selection set. This ensures we don't accidentally select the field twice in our query. +Now that we've determined we are working with the `currentUser` field, we need to figure out if our `id` field is already part of the `currentUser` field's selection set. This ensures we don't accidentally select the field twice in our query. To do so, let's get the field's `selectionSet` property and loop over its `selections` property to determine if the `id` field is included. -It's important to note that a `selectionSet` may contain `selections` of both fields and fragments. Our implementation only needs to perform checks against fields, so we also check the selection's `kind` property. If we find a match on a field named `id`, we can stop traversal of the AST. +It's important to note that a `selectionSet` may contain `selections` of both fields and fragments. Our implementation only needs to perform checks against fields, so we also check the selection's `kind` property. If we find a match on a field named `id`, we can stop traversal of the AST. We will bring in both the [`Kind`](https://graphql.org/graphql-js/language/#kind) enum from `graphql-js`, which allows us to compare against the selection's `kind` property, and the [`BREAK`](https://graphql.org/graphql-js/language/#break) sentinel, which directs the `visit` function to stop traversal of the AST. @@ -160,7 +161,7 @@ const transformedDocument = visit(document, { Field(field) { // ... const idField = { - // ... + // ... }; return { @@ -224,7 +225,7 @@ const documentTransform = new DocumentTransform((document) => { ### Check our document transform -We can check our custom document transform by calling the `transformDocument` function and passing a GraphQL query to it. +We can check our custom document transform by calling the `transformDocument` function and passing a GraphQL query to it. ```ts import { print } from 'graphql'; @@ -331,7 +332,7 @@ Here `documentTransform1` is combined with `documentTransform2` into a single do #### A note about performance -Combining multiple transforms is a powerful feature that makes it easy to split up transform logic, which can boost maintainability. Depending on the implementation of your visitor, this can result in the traversal of the GraphQL document AST multiple times. Most of the time, this shouldn't be an issue. We recommend using the [`BREAK`](https://graphql.org/graphql-js/language/#break) sentinel from `graphql-js` to prevent unnecessary traversal. +Combining multiple transforms is a powerful feature that makes it easy to split up transform logic, which can boost maintainability. Depending on the implementation of your visitor, this can result in the traversal of the GraphQL document AST multiple times. Most of the time, this shouldn't be an issue. We recommend using the [`BREAK`](https://graphql.org/graphql-js/language/#break) sentinel from `graphql-js` to prevent unnecessary traversal. Suppose you are sending very large queries that require several traversals and have already optimized your visitors with the `BREAK` sentinel. In that case, it's best to combine the transforms into a single visitor that traverses the AST once. @@ -464,7 +465,7 @@ const documentTransform = new DocumentTransform( getCacheKey: (document) => { // Always run the transform function when `shouldCache` is `false` if (shouldCache(document)) { - return [document] + return [document] } } } @@ -507,7 +508,7 @@ const nonCachedTransform = new DocumentTransform(transform, { cache: false }); -const documentTransform = +const documentTransform = cachedTransform .concat(varyingTransform) .concat(conditionalCachedTransform) @@ -527,7 +528,7 @@ Thankfully, GraphQL Code Generator provides a [document transform](https://the-g ```ts title="codegen.ts" {2,12-18} import type { CodegenConfig } from '@graphql-codegen/cli'; import { documentTransform } from './path/to/your/transform'; - + const config: CodegenConfig = { schema: 'https://localhost:4000/graphql', documents: ['src/**/*.tsx'], @@ -574,7 +575,7 @@ Here is an example that uses a DSL-like directive that depends on a feature flag ```ts const query = gql` query MyQuery { - myCustomField @feature(name: "custom", version: 2) + myCustomField @feature(name: "custom", version: 2) } `; @@ -584,7 +585,7 @@ const documentTransform = new DocumentTransform((document) => { documentTransform.transformDocument(query); // query MyQuery($feature_custom_v2: Boolean!) { -// myCustomField @include(if: $feature_custom_v2) +// myCustomField @include(if: $feature_custom_v2) // } ``` @@ -592,4 +593,4 @@ documentTransform.transformDocument(query); ### Options - + diff --git a/docs/source/data/mutations.mdx b/docs/source/data/mutations.mdx index 3a6eec83618..8a7c4c806cf 100644 --- a/docs/source/data/mutations.mdx +++ b/docs/source/data/mutations.mdx @@ -1,10 +1,12 @@ --- title: Mutations in Apollo Client description: Modify data with the useMutation hook +api_doc: + - "@apollo/client!MutationHookOptions:interface" + - "@apollo/client!MutationResult:interface" --- -import MutationOptions3 from '../../shared/mutation-options.mdx'; -import MutationResult3 from '../../shared/mutation-result.mdx'; +import { PropertySignatureTable } from '../../shared/ApiDoc'; Now that we've [learned how to query data](queries/) from our backend with Apollo Client, the natural next step is to learn how to _modify_ back-end data with **mutations**. @@ -380,7 +382,7 @@ detail with usage examples, see the [API reference](../api/react/hooks/). The `useMutation` hook accepts the following options: - + ### Result @@ -388,7 +390,7 @@ The `useMutation` result is a tuple with a mutate function in the first position You call the mutate function to trigger the mutation from your UI. - + ## Next steps diff --git a/docs/source/data/queries.mdx b/docs/source/data/queries.mdx index 4814d615370..5ee6ae03d49 100644 --- a/docs/source/data/queries.mdx +++ b/docs/source/data/queries.mdx @@ -1,10 +1,12 @@ --- title: Queries description: Fetch data with the useQuery hook +api_doc: + - "@apollo/client!QueryHookOptions:interface" + - "@apollo/client!QueryResult:interface" --- -import QueryOptions from '../../shared/query-options.mdx'; -import QueryResult from '../../shared/query-result.mdx'; +import { PropertySignatureTable } from '../../shared/ApiDoc'; This article shows how to fetch GraphQL data in React with the `useQuery` hook and attach the result to your UI. You'll also learn how Apollo Client simplifies data management code by tracking error and loading states for you. @@ -504,13 +506,13 @@ Most calls to `useQuery` can omit the majority of these options, but it's useful The `useQuery` hook accepts the following options: - + ### Result After being called, the `useQuery` hook returns a result object with the following properties. This object contains your query result, plus some helpful functions for refetching, dynamic polling, and pagination. - + ## Next steps diff --git a/docs/source/data/subscriptions.mdx b/docs/source/data/subscriptions.mdx index 1c48ac253ca..59e451c44ee 100644 --- a/docs/source/data/subscriptions.mdx +++ b/docs/source/data/subscriptions.mdx @@ -1,10 +1,12 @@ --- title: Subscriptions description: Get real-time updates from your GraphQL server +api_doc: + - "@apollo/client!SubscriptionHookOptions:interface" + - "@apollo/client!SubscriptionResult:interface" --- -import SubscriptionOptions from '../../shared/subscription-options.mdx'; -import SubscriptionResult from '../../shared/subscription-result.mdx'; +import { PropertySignatureTable } from '../../shared/ApiDoc'; In addition to [queries](./queries/) and [mutations](./mutations/), GraphQL supports a third operation type: **subscriptions**. @@ -373,13 +375,13 @@ export function CommentsPage({subscribeToNewComments}) { The `useSubscription` Hook accepts the following options: - + ### Result After being called, the `useSubscription` Hook returns a result object with the following properties: - + ## The older `subscriptions-transport-ws` library diff --git a/netlify.toml b/netlify.toml index 67879a8b0ba..2a741ec142c 100644 --- a/netlify.toml +++ b/netlify.toml @@ -13,7 +13,7 @@ npm run docmodel cd ../ rm -rf monodocs - git clone https://github.com/apollographql/docs --branch main --single-branch monodocs + git clone https://github.com/apollographql/docs --branch pr/apidoc-enums-since --single-branch monodocs cd monodocs npm i cp -r ../docs local diff --git a/src/cache/core/types/DataProxy.ts b/src/cache/core/types/DataProxy.ts index d340f187d4e..4e3e0f9cc73 100644 --- a/src/cache/core/types/DataProxy.ts +++ b/src/cache/core/types/DataProxy.ts @@ -68,18 +68,7 @@ export namespace DataProxy { * readQuery method can be omitted. Defaults to false. */ optimistic?: boolean; - /** - /** - * @deprecated - * Using `canonizeResults` can result in memory leaks so we generally do not - * recommend using this option anymore. - * A future version of Apollo Client will contain a similar feature without - * the risk of memory leaks. - * - * Whether to canonize cache results before returning them. Canonization - * takes some extra time, but it speeds up future deep equality comparisons. - * Defaults to false. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ canonizeResults?: boolean; } @@ -96,16 +85,7 @@ export namespace DataProxy { * readQuery method can be omitted. Defaults to false. */ optimistic?: boolean; - /** - * @deprecated - * Using `canonizeResults` can result in memory leaks so we generally do not - * recommend using this option anymore. - * A future version of Apollo Client will contain a similar feature. - * - * Whether to canonize cache results before returning them. Canonization - * takes some extra time, but it speeds up future deep equality comparisons. - * Defaults to false. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ canonizeResults?: boolean; } diff --git a/src/cache/inmemory/types.ts b/src/cache/inmemory/types.ts index bd05ff7aacf..10678ce5ad7 100644 --- a/src/cache/inmemory/types.ts +++ b/src/cache/inmemory/types.ts @@ -147,7 +147,6 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { /** * @deprecated * Please use `cacheSizes` instead. - * TODO: write docs page, add link here */ resultCacheMaxSize?: number; /** diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index a0b33993d61..55bae5b11a9 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -76,7 +76,7 @@ export interface ApolloClientOptions { */ ssrForceFetchDelay?: number; /** - * When using Apollo Client for [server-side rendering](https://www.apollographql.com/docs/react//performance/server-side-rendering/), set this to `true` so that the [`getDataFromTree` function](../react/ssr/#getdatafromtree) can work effectively. + * When using Apollo Client for [server-side rendering](https://www.apollographql.com/docs/react/performance/server-side-rendering/), set this to `true` so that the [`getDataFromTree` function](../react/ssr/#getdatafromtree) can work effectively. * * @defaultValue `false` */ diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 03418c0e1d9..2b42f3b044e 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -80,6 +80,9 @@ export class ObservableQuery< // Computed shorthand for this.options.variables, preserved for // backwards compatibility. + /** + * An object containing the variables that were provided for the query. + */ public get variables(): TVariables | undefined { return this.options.variables; } @@ -417,6 +420,9 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, return this.reobserve(reobserveOptions, NetworkStatus.refetch); } + /** + * A function that helps you fetch the next set of results for a [paginated list field](https://www.apollographql.com/docs/react/pagination/core-api/). + */ public fetchMore< TFetchData = TData, TFetchVars extends OperationVariables = TVariables, @@ -545,6 +551,11 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, // XXX the subscription variables are separate from the query variables. // if you want to update subscription variables, right now you have to do that separately, // and you can only do it by stopping the subscription and then subscribing again with new variables. + /** + * A function that enables you to execute a [subscription](https://www.apollographql.com/docs/react/data/subscriptions/), usually to subscribe to specific fields that were included in the query. + * + * This function returns _another_ function that you can call to terminate the subscription. + */ public subscribeToMore< TSubscriptionData = TData, TSubscriptionVariables extends OperationVariables = TVariables, @@ -650,6 +661,11 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, ); } + /** + * A function that enables you to update the query's cached result without executing a followup GraphQL operation. + * + * See [using updateQuery and updateFragment](https://www.apollographql.com/docs/react/caching/cache-interaction/#using-updatequery-and-updatefragment) for additional information. + */ public updateQuery( mapFn: ( previousQueryResult: TData, @@ -679,11 +695,17 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, } } + /** + * A function that instructs the query to begin re-executing at a specified interval (in milliseconds). + */ public startPolling(pollInterval: number) { this.options.pollInterval = pollInterval; this.updatePolling(); } + /** + * A function that instructs the query to stop polling after a previous call to `startPolling`. + */ public stopPolling() { this.options.pollInterval = 0; this.updatePolling(); diff --git a/src/core/types.ts b/src/core/types.ts index eeda05fa41c..8085d013839 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -139,7 +139,7 @@ export type { QueryOptions as PureQueryOptions }; export type OperationVariables = Record; -export type ApolloQueryResult = { +export interface ApolloQueryResult { data: T; /** * A list of any errors that occurred during server-side execution of a GraphQL operation. @@ -158,7 +158,7 @@ export type ApolloQueryResult = { // result.partial will be true. Otherwise, result.partial will be falsy // (usually because the property is absent from the result object). partial?: boolean; -}; +} // This is part of the public API, people write these functions in `updateQueries`. export type MutationQueryReducer = ( @@ -174,7 +174,9 @@ export type MutationQueryReducersMap = { [queryName: string]: MutationQueryReducer; }; -// @deprecated Use MutationUpdaterFunction instead. +/** + * @deprecated Use `MutationUpdaterFunction` instead. + */ export type MutationUpdaterFn = ( // The MutationUpdaterFn type is broken because it mistakenly uses the same // type parameter T for both the cache and the mutationResult. Do not use this diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index c4844826c75..5810c6464c4 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -52,70 +52,34 @@ export type ErrorPolicy = "none" | "ignore" | "all"; * Query options. */ export interface QueryOptions { - /** - * A GraphQL document that consists of a single query to be sent down to the - * server. - */ - // TODO REFACTOR: rename this to document. Didn't do it yet because it's in a - // lot of tests. + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ query: DocumentNode | TypedDocumentNode; - /** - * A map going from variable name to variable value, where the variables are used - * within the GraphQL query. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables?: TVariables; - /** - * Specifies the {@link ErrorPolicy} to be used for this query - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** - * Context to be passed to link execution chain - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; - /** - * Specifies the {@link FetchPolicy} to be used for this query - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: FetchPolicy; - /** - * The time interval (in milliseconds) on which this query should be - * refetched from the server. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#pollInterval:member} */ pollInterval?: number; - /** - * Whether or not updates to the network status should trigger next on the observer of this query - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#notifyOnNetworkStatusChange:member} */ notifyOnNetworkStatusChange?: boolean; - /** - * Allow returning incomplete data from the cache when a larger query cannot - * be fully satisfied by the cache, instead of returning nothing. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#returnPartialData:member} */ returnPartialData?: boolean; - /** - * If `true`, perform a query `refetch` if the query result is marked as - * being partial, and the returned data is reset to an empty Object by the - * Apollo Client `QueryManager` (due to a cache miss). - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#partialRefetch:member} */ partialRefetch?: boolean; - /** - * @deprecated - * Using `canonizeResults` can result in memory leaks so we generally do not - * recommend using this option anymore. - * A future version of Apollo Client will contain a similar feature without - * the risk of memory leaks. - * - * Whether to canonize cache results before returning them. Canonization - * takes some extra time, but it speeds up future deep equality comparisons. - * Defaults to false. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ canonizeResults?: boolean; } @@ -125,15 +89,19 @@ export interface QueryOptions { export interface WatchQueryOptions< TVariables extends OperationVariables = OperationVariables, TData = any, +> extends SharedWatchQueryOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ + query: DocumentNode | TypedDocumentNode; +} + +export interface SharedWatchQueryOptions< + TVariables extends OperationVariables, + TData, > { - /** - * Specifies the {@link FetchPolicy} to be used for this query. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: WatchQueryFetchPolicy; - /** - * Specifies the {@link FetchPolicy} to be used after this query has completed. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#nextFetchPolicy:member} */ nextFetchPolicy?: | WatchQueryFetchPolicy | (( @@ -142,53 +110,37 @@ export interface WatchQueryOptions< context: NextFetchPolicyContext ) => WatchQueryFetchPolicy); - /** - * Defaults to the initial value of options.fetchPolicy, but can be explicitly - * configured to specify the WatchQueryFetchPolicy to revert back to whenever - * variables change (unless nextFetchPolicy intervenes). - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#initialFetchPolicy:member} */ initialFetchPolicy?: WatchQueryFetchPolicy; - /** - * Specifies whether a {@link NetworkStatus.refetch} operation should merge - * incoming field data with existing data, or overwrite the existing data. - * Overwriting is probably preferable, but merging is currently the default - * behavior, for backwards compatibility with Apollo Client 3.x. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#refetchWritePolicy:member} */ refetchWritePolicy?: RefetchWritePolicy; - /** {@inheritDoc @apollo/client!QueryOptions#query:member} */ - query: DocumentNode | TypedDocumentNode; - - /** {@inheritDoc @apollo/client!QueryOptions#variables:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables?: TVariables; - /** {@inheritDoc @apollo/client!QueryOptions#errorPolicy:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** {@inheritDoc @apollo/client!QueryOptions#context:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; - /** {@inheritDoc @apollo/client!QueryOptions#pollInterval:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#pollInterval:member} */ pollInterval?: number; - /** {@inheritDoc @apollo/client!QueryOptions#notifyOnNetworkStatusChange:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#notifyOnNetworkStatusChange:member} */ notifyOnNetworkStatusChange?: boolean; - /** {@inheritDoc @apollo/client!QueryOptions#returnPartialData:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#returnPartialData:member} */ returnPartialData?: boolean; - /** {@inheritDoc @apollo/client!QueryOptions#partialRefetch:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#partialRefetch:member} */ partialRefetch?: boolean; - /** {@inheritDoc @apollo/client!QueryOptions#canonizeResults:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ canonizeResults?: boolean; - /** - * A callback function that's called whenever a refetch attempt occurs - * while polling. If the function returns `true`, the refetch is - * skipped and not reattempted until the next poll interval. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#skipPollAttempt:member} */ skipPollAttempt?: () => boolean; } @@ -203,7 +155,9 @@ export interface NextFetchPolicyContext< } export interface FetchMoreQueryOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ query?: DocumentNode | TypedDocumentNode; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables?: Partial; context?: DefaultContext; } @@ -238,31 +192,19 @@ export interface SubscriptionOptions< TVariables = OperationVariables, TData = any, > { - /** - * A GraphQL document, often created with `gql` from the `graphql-tag` - * package, that contains a single subscription inside of it. - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#query:member} */ query: DocumentNode | TypedDocumentNode; - /** - * An object that maps from the name of a variable as used in the subscription - * GraphQL document to that variable's value. - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#variables:member} */ variables?: TVariables; - /** - * Specifies the {@link FetchPolicy} to be used for this subscription. - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: FetchPolicy; - /** - * Specifies the {@link ErrorPolicy} to be used for this operation - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** - * Context object to be passed through the link execution chain. - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#context:member} */ context?: DefaultContext; } @@ -272,94 +214,35 @@ export interface MutationBaseOptions< TContext = DefaultContext, TCache extends ApolloCache = ApolloCache, > { - /** - * An object that represents the result of this mutation that will be - * optimistically stored before the server has actually returned a result. - * This is most often used for optimistic UI, where we want to be able to see - * the result of a mutation immediately, and update the UI later if any errors - * appear. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#optimisticResponse:member} */ optimisticResponse?: | TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier }) => TData); - /** - * A {@link MutationQueryReducersMap}, which is map from query names to - * mutation query reducers. Briefly, this map defines how to incorporate the - * results of the mutation into the results of queries that are currently - * being watched by your application. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#updateQueries:member} */ updateQueries?: MutationQueryReducersMap; - /** - * A list of query names which will be refetched once this mutation has - * returned. This is often used if you have a set of queries which may be - * affected by a mutation and will have to update. Rather than writing a - * mutation query reducer (i.e. `updateQueries`) for this, you can simply - * refetch the queries that will be affected and achieve a consistent store - * once these queries return. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#refetchQueries:member} */ refetchQueries?: | ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; - /** - * By default, `refetchQueries` does not wait for the refetched queries to - * be completed, before resolving the mutation `Promise`. This ensures that - * query refetching does not hold up mutation response handling (query - * refetching is handled asynchronously). Set `awaitRefetchQueries` to - * `true` if you would like to wait for the refetched queries to complete, - * before the mutation can be marked as resolved. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#awaitRefetchQueries:member} */ awaitRefetchQueries?: boolean; - /** - * A function which provides an {@link ApolloCache} instance, and the result - * of the mutation, to allow the user to update the store based on the - * results of the mutation. - * - * This function will be called twice over the lifecycle of a mutation. Once - * at the very beginning if an `optimisticResponse` was provided. The writes - * created from the optimistic data will be rolled back before the second time - * this function is called which is when the mutation has successfully - * resolved. At that point `update` will be called with the *actual* mutation - * result and those writes will not be rolled back. - * - * Note that since this function is intended to be used to update the - * store, it cannot be used with a `no-cache` fetch policy. If you're - * interested in performing some action after a mutation has completed, - * and you don't need to update the store, use the Promise returned from - * `client.mutate` instead. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#update:member} */ update?: MutationUpdaterFunction; - /** - * A function that will be called for each ObservableQuery affected by - * this mutation, after the mutation has completed. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#onQueryUpdated:member} */ onQueryUpdated?: OnQueryUpdated; - /** - * Specifies the {@link ErrorPolicy} to be used for this operation - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** - * An object that maps from the name of a variable as used in the mutation - * GraphQL document to that variable's value. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#variables:member} */ variables?: TVariables; - /** - * The context to be passed to the link execution chain. This context will - * only be used with this mutation. It will not be used with - * `refetchQueries`. Refetched queries use the context they were - * initialized with (since the initial context is stored as part of the - * `ObservableQuery` instance). If a specific context is needed when - * refetching queries, make sure it is configured (via the - * [query `context` option](https://www.apollographql.com/docs/react/api/apollo-client#ApolloClient.query)) - * when the query is first initialized/run. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#context:member} */ context?: TContext; } @@ -368,28 +251,19 @@ export interface MutationOptions< TVariables = OperationVariables, TContext = DefaultContext, TCache extends ApolloCache = ApolloCache, -> extends MutationBaseOptions { - /** - * A GraphQL document, often created with `gql` from the `graphql-tag` - * package, that contains a single mutation inside of it. - */ +> extends MutationSharedOptions { + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#mutation:member} */ mutation: DocumentNode | TypedDocumentNode; - - /** - * Specifies the {@link MutationFetchPolicy} to be used for this query. - * Mutations support only 'network-only' and 'no-cache' fetchPolicy strings. - * If fetchPolicy is not provided, it defaults to 'network-only'. - */ +} +export interface MutationSharedOptions< + TData = any, + TVariables = OperationVariables, + TContext = DefaultContext, + TCache extends ApolloCache = ApolloCache, +> extends MutationBaseOptions { + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: MutationFetchPolicy; - /** - * To avoid retaining sensitive information from mutation root field - * arguments, Apollo Client v3.4+ automatically clears any `ROOT_MUTATION` - * fields from the cache after each mutation finishes. If you need this - * information to remain in the cache, you can prevent the removal by passing - * `keepRootFields: true` to the mutation. `ROOT_MUTATION` result data are - * also passed to the mutation `update` function, so we recommend obtaining - * the results that way, rather than using this option, if possible. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#keepRootFields:member} */ keepRootFields?: boolean; } diff --git a/src/react/components/types.ts b/src/react/components/types.ts index a0114f65ae2..a742b905ac6 100644 --- a/src/react/components/types.ts +++ b/src/react/components/types.ts @@ -45,6 +45,7 @@ export interface SubscriptionComponentOptions< TData = any, TVariables extends OperationVariables = OperationVariables, > extends BaseSubscriptionOptions { + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#query:member} */ subscription: DocumentNode | TypedDocumentNode; children?: | null diff --git a/src/react/hooks/useApolloClient.ts b/src/react/hooks/useApolloClient.ts index 54bf7bd0874..8f4fdc80f0c 100644 --- a/src/react/hooks/useApolloClient.ts +++ b/src/react/hooks/useApolloClient.ts @@ -3,6 +3,21 @@ import * as React from "rehackt"; import type { ApolloClient } from "../../core/index.js"; import { getApolloContext } from "../context/index.js"; +/** + * @example + * ```jsx + * import { useApolloClient } from '@apollo/client'; + * + * function SomeComponent() { + * const client = useApolloClient(); + * // `client` is now set to the `ApolloClient` instance being used by the + * // application (that was configured using something like `ApolloProvider`) + * } + * ``` + * + * @since 3.0.0 + * @returns The `ApolloClient` instance being used by the application. + */ export function useApolloClient( override?: ApolloClient ): ApolloClient { diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index e0f237dffbc..6c58c86e3ce 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -25,6 +25,41 @@ const EAGER_METHODS = [ "subscribeToMore", ] as const; +/** + * A hook for imperatively executing queries in an Apollo application, e.g. in response to user interaction. + * + * > Refer to the [Queries - Manual execution with useLazyQuery](https://www.apollographql.com/docs/react/data/queries#manual-execution-with-uselazyquery) section for a more in-depth overview of `useLazyQuery`. + * + * @example + * ```jsx + * import { gql, useLazyQuery } from "@apollo/client"; + * + * const GET_GREETING = gql` + * query GetGreeting($language: String!) { + * greeting(language: $language) { + * message + * } + * } + * `; + * + * function Hello() { + * const [loadGreeting, { called, loading, data }] = useLazyQuery( + * GET_GREETING, + * { variables: { language: "english" } } + * ); + * if (called && loading) return

Loading ...

+ * if (!called) { + * return + * } + * return

Hello {data.greeting.message}!

; + * } + * ``` + * @since 3.0.0 + * + * @param query - A GraphQL query document parsed into an AST by `gql`. + * @param options - Default options to control how the query is executed. + * @returns A tuple in the form of `[execute, result]` + */ export function useLazyQuery< TData = any, TVariables extends OperationVariables = OperationVariables, diff --git a/src/react/hooks/useMutation.ts b/src/react/hooks/useMutation.ts index ac3d3cfea8f..79825f91524 100644 --- a/src/react/hooks/useMutation.ts +++ b/src/react/hooks/useMutation.ts @@ -21,6 +21,53 @@ import { DocumentType, verifyDocumentType } from "../parser/index.js"; import { ApolloError } from "../../errors/index.js"; import { useApolloClient } from "./useApolloClient.js"; +/** + * + * + * > Refer to the [Mutations](https://www.apollographql.com/docs/react/data/mutations/) section for a more in-depth overview of `useMutation`. + * + * @example + * ```jsx + * import { gql, useMutation } from '@apollo/client'; + * + * const ADD_TODO = gql` + * mutation AddTodo($type: String!) { + * addTodo(type: $type) { + * id + * type + * } + * } + * `; + * + * function AddTodo() { + * let input; + * const [addTodo, { data }] = useMutation(ADD_TODO); + * + * return ( + *
+ *
{ + * e.preventDefault(); + * addTodo({ variables: { type: input.value } }); + * input.value = ''; + * }} + * > + * { + * input = node; + * }} + * /> + * + *
+ *
+ * ); + * } + * ``` + * @since 3.0.0 + * @param mutation - A GraphQL mutation document parsed into an AST by `gql`. + * @param options - Options to control how the mutation is executed. + * @returns A tuple in the form of `[mutate, result]` + */ export function useMutation< TData = any, TVariables = OperationVariables, diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index b83f8558888..4bc596ed371 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -41,6 +41,40 @@ const { prototype: { hasOwnProperty }, } = Object; +/** + * A hook for executing queries in an Apollo application. + * + * To run a query within a React component, call `useQuery` and pass it a GraphQL query document. + * + * When your component renders, `useQuery` returns an object from Apollo Client that contains `loading`, `error`, and `data` properties you can use to render your UI. + * + * > Refer to the [Queries](https://www.apollographql.com/docs/react/data/queries) section for a more in-depth overview of `useQuery`. + * + * @example + * ```jsx + * import { gql, useQuery } from '@apollo/client'; + * + * const GET_GREETING = gql` + * query GetGreeting($language: String!) { + * greeting(language: $language) { + * message + * } + * } + * `; + * + * function Hello() { + * const { loading, error, data } = useQuery(GET_GREETING, { + * variables: { language: 'english' }, + * }); + * if (loading) return

Loading ...

; + * return

Hello {data.greeting.message}!

; + * } + * ``` + * @since 3.0.0 + * @param query - A GraphQL query document parsed into an AST by `gql`. + * @param options - Options to control how the query is executed. + * @returns Query result object + */ export function useQuery< TData = any, TVariables extends OperationVariables = OperationVariables, diff --git a/src/react/hooks/useReactiveVar.ts b/src/react/hooks/useReactiveVar.ts index b98c4401e69..ed7ac2379d3 100644 --- a/src/react/hooks/useReactiveVar.ts +++ b/src/react/hooks/useReactiveVar.ts @@ -2,6 +2,23 @@ import * as React from "rehackt"; import type { ReactiveVar } from "../../core/index.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; +/** + * Reads the value of a [reactive variable](https://www.apollographql.com/docs/react/local-state/reactive-variables/) and re-renders the containing component whenever that variable's value changes. This enables a reactive variable to trigger changes _without_ relying on the `useQuery` hook. + * + * @example + * ```jsx + * import { makeVar, useReactiveVar } from "@apollo/client"; + * export const cartItemsVar = makeVar([]); + * + * export function Cart() { + * const cartItems = useReactiveVar(cartItemsVar); + * // ... + * } + * ``` + * @since 3.2.0 + * @param rv - A reactive variable. + * @returns The current value of the reactive variable. + */ export function useReactiveVar(rv: ReactiveVar): T { return useSyncExternalStore( React.useCallback( diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts index 33029adbb9a..366ebfe97f4 100644 --- a/src/react/hooks/useSubscription.ts +++ b/src/react/hooks/useSubscription.ts @@ -12,7 +12,91 @@ import type { } from "../types/types.js"; import type { OperationVariables } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; - +/** + * > Refer to the [Subscriptions](https://www.apollographql.com/docs/react/data/subscriptions/) section for a more in-depth overview of `useSubscription`. + * + * @example + * ```jsx + * const COMMENTS_SUBSCRIPTION = gql` + * subscription OnCommentAdded($repoFullName: String!) { + * commentAdded(repoFullName: $repoFullName) { + * id + * content + * } + * } + * `; + * + * function DontReadTheComments({ repoFullName }) { + * const { + * data: { commentAdded }, + * loading, + * } = useSubscription(COMMENTS_SUBSCRIPTION, { variables: { repoFullName } }); + * return

New comment: {!loading && commentAdded.content}

; + * } + * ``` + * @remarks + * #### Subscriptions and React 18 Automatic Batching + * + * With React 18's [automatic batching](https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching), multiple state updates may be grouped into a single re-render for better performance. + * + * If your subscription API sends multiple messages at the same time or in very fast succession (within fractions of a millisecond), it is likely that only the last message received in that narrow time frame will result in a re-render. + * + * Consider the following component: + * + * ```jsx + * export function Subscriptions() { + * const { data, error, loading } = useSubscription(query); + * const [accumulatedData, setAccumulatedData] = useState([]); + * + * useEffect(() => { + * setAccumulatedData((prev) => [...prev, data]); + * }, [data]); + * + * return ( + * <> + * {loading &&

Loading...

} + * {JSON.stringify(accumulatedData, undefined, 2)} + * + * ); + * } + * ``` + * + * If your subscription back-end emits two messages with the same timestamp, only the last message received by Apollo Client will be rendered. This is because React 18 will batch these two state updates into a single re-render. + * + * Since the component above is using `useEffect` to push `data` into a piece of local state on each `Subscriptions` re-render, the first message will never be added to the `accumulatedData` array since its render was skipped. + * + * Instead of using `useEffect` here, we can re-write this component to use the `onData` callback function accepted in `useSubscription`'s `options` object: + * + * ```jsx + * export function Subscriptions() { + * const [accumulatedData, setAccumulatedData] = useState([]); + * const { data, error, loading } = useSubscription( + * query, + * { + * onData({ data }) { + * setAccumulatedData((prev) => [...prev, data]) + * } + * } + * ); + * + * return ( + * <> + * {loading &&

Loading...

} + * {JSON.stringify(accumulatedData, undefined, 2)} + * + * ); + * } + * ``` + * + * > ⚠️ **Note:** The `useSubscription` option `onData` is available in Apollo Client >= 3.7. In previous versions, the equivalent option is named `onSubscriptionData`. + * + * Now, the first message will be added to the `accumulatedData` array since `onData` is called _before_ the component re-renders. React 18 automatic batching is still in effect and results in a single re-render, but with `onData` we can guarantee each message received after the component mounts is added to `accumulatedData`. + * + * @since 3.0.0 + * @param subscription - A GraphQL subscription document parsed into an AST by `gql`. + * @param options - Options to control how the subscription is executed. + * @returns Query result object + */ export function useSubscription< TData = any, TVariables extends OperationVariables = OperationVariables, diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index f5a3a03ab05..12b7efd7983 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -20,16 +20,16 @@ import type { NoInfer } from "../index.js"; type VariablesOption = [TVariables] extends [never] ? { - /** {@inheritDoc @apollo/client!QueryOptions#variables:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables?: Record; } : {} extends OnlyRequiredProperties ? { - /** {@inheritDoc @apollo/client!QueryOptions#variables:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables?: TVariables; } : { - /** {@inheritDoc @apollo/client!QueryOptions#variables:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables: TVariables; }; @@ -41,17 +41,17 @@ export type PreloadQueryFetchPolicy = Extract< export type PreloadQueryOptions< TVariables extends OperationVariables = OperationVariables, > = { - /** {@inheritDoc @apollo/client!QueryOptions#canonizeResults:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ canonizeResults?: boolean; - /** {@inheritDoc @apollo/client!QueryOptions#context:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; - /** {@inheritDoc @apollo/client!QueryOptions#errorPolicy:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** {@inheritDoc @apollo/client!QueryOptions#fetchPolicy:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: PreloadQueryFetchPolicy; - /** {@inheritDoc @apollo/client!QueryOptions#returnPartialData:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#returnPartialData:member} */ returnPartialData?: boolean; - /** {@inheritDoc @apollo/client!WatchQueryOptions#refetchWritePolicy:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#refetchWritePolicy:member} */ refetchWritePolicy?: RefetchWritePolicy; } & VariablesOption; diff --git a/src/react/types/types.documentation.ts b/src/react/types/types.documentation.ts new file mode 100644 index 00000000000..364e8e3f188 --- /dev/null +++ b/src/react/types/types.documentation.ts @@ -0,0 +1,598 @@ +export interface QueryOptionsDocumentation { + /** + * A GraphQL query string parsed into an AST with the gql template literal. + * + * @docGroup 1. Operation options + */ + query: unknown; + + /** + * An object containing all of the GraphQL variables your query requires to execute. + * + * Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. + * + * @docGroup 1. Operation options + */ + variables: unknown; + + /** + * Specifies how the query handles a response that returns both GraphQL errors and partial results. + * + * For details, see [GraphQL error policies](https://www.apollographql.com/docs/react/data/error-handling/#graphql-error-policies). + * + * The default value is `none`, meaning that the query result includes error details but not partial results. + * + * @docGroup 1. Operation options + */ + errorPolicy: unknown; + + /** + * If you're using [Apollo Link](https://www.apollographql.com/docs/react/api/link/introduction/), this object is the initial value of the `context` object that's passed along your link chain. + * + * @docGroup 2. Networking options + */ + context: unknown; + + /** + * Specifies how the query interacts with the Apollo Client cache during execution (for example, whether it checks the cache for results before sending a request to the server). + * + * For details, see [Setting a fetch policy](https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy). + * + * The default value is `cache-first`. + * + * @docGroup 3. Caching options + */ + fetchPolicy: unknown; + + /** + * Specifies the {@link FetchPolicy} to be used after this query has completed. + * + * @docGroup 3. Caching options + */ + nextFetchPolicy: unknown; + + /** + * Defaults to the initial value of options.fetchPolicy, but can be explicitly + * configured to specify the WatchQueryFetchPolicy to revert back to whenever + * variables change (unless nextFetchPolicy intervenes). + * + * @docGroup 3. Caching options + */ + initialFetchPolicy: unknown; + + /** + * Specifies the interval (in milliseconds) at which the query polls for updated results. + * + * The default value is `0` (no polling). + * + * @docGroup 2. Networking options + */ + pollInterval: unknown; + + /** + * If `true`, the in-progress query's associated component re-renders whenever the network status changes or a network error occurs. + * + * The default value is `false`. + * + * @docGroup 2. Networking options + */ + notifyOnNetworkStatusChange: unknown; + + /** + * If `true`, the query can return partial results from the cache if the cache doesn't contain results for all queried fields. + * + * The default value is `false`. + * + * @docGroup 3. Caching options + */ + returnPartialData: unknown; + + /** + * Specifies whether a `NetworkStatus.refetch` operation should merge + * incoming field data with existing data, or overwrite the existing data. + * Overwriting is probably preferable, but merging is currently the default + * behavior, for backwards compatibility with Apollo Client 3.x. + * + * @docGroup 3. Caching options + */ + refetchWritePolicy: unknown; + + /** + * Watched queries must opt into overwriting existing data on refetch, by passing refetchWritePolicy: "overwrite" in their WatchQueryOptions. + * + * The default value is "overwrite". + * + * @docGroup 3. Caching options + */ + refetchWritePolicy_suspense: unknown; + + /** + * If `true`, causes a query refetch if the query result is detected as partial. + * + * The default value is `false`. + * + * @deprecated + * Setting this option is unnecessary in Apollo Client 3, thanks to a more consistent application of fetch policies. It might be removed in a future release. + */ + partialRefetch: unknown; + + /** + * Whether to canonize cache results before returning them. Canonization + * takes some extra time, but it speeds up future deep equality comparisons. + * Defaults to false. + * + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature without + * the risk of memory leaks. + */ + canonizeResults: unknown; + + /** + * If true, the query is not executed. + * + * The default value is `false`. + * + * @docGroup 1. Operation options + */ + skip: unknown; + + /** + * If `true`, the query is not executed. The default value is `false`. + * + * @deprecated We recommend using `skipToken` in place of the `skip` option as + * it is more type-safe. + * + * This option is deprecated and only supported to ease the migration from useQuery. It will be removed in a future release. + * + * @docGroup 1. Operation options + */ + skip_deprecated: unknown; + + /** + * A callback function that's called when your query successfully completes with zero errors (or if `errorPolicy` is `ignore` and partial data is returned). + * + * This function is passed the query's result `data`. + * + * @docGroup 1. Operation options + */ + onCompleted: unknown; + /** + * A callback function that's called when the query encounters one or more errors (unless `errorPolicy` is `ignore`). + * + * This function is passed an `ApolloError` object that contains either a `networkError` object or a `graphQLErrors` array, depending on the error(s) that occurred. + * + * @docGroup 1. Operation options + */ + onError: unknown; + + /** + * The instance of {@link ApolloClient} to use to execute the query. + * + * By default, the instance that's passed down via context is used, but you + * can provide a different instance here. + * + * @docGroup 1. Operation options + */ + client: unknown; + + /** + * A unique identifier for the query. Each item in the array must be a stable + * identifier to prevent infinite fetches. + * + * This is useful when using the same query and variables combination in more + * than one component, otherwise the components may clobber each other. This + * can also be used to force the query to re-evaluate fresh. + * + * @docGroup 1. Operation options + */ + queryKey: unknown; + + /** + * Pass `false` to skip executing the query during [server-side rendering](https://www.apollographql.com/docs/react/performance/server-side-rendering/). + * + * @docGroup 2. Networking options + */ + ssr: unknown; + + /** + * A callback function that's called whenever a refetch attempt occurs + * while polling. If the function returns `true`, the refetch is + * skipped and not reattempted until the next poll interval. + * + * @docGroup 2. Networking options + */ + skipPollAttempt: unknown; +} + +export interface QueryResultDocumentation { + /** + * The instance of Apollo Client that executed the query. + * Can be useful for manually executing followup queries or writing data to the cache. + * + * @docGroup 2. Network info + */ + client: unknown; + /** + * A reference to the internal `ObservableQuery` used by the hook. + */ + observable: unknown; + /** + * An object containing the result of your GraphQL query after it completes. + * + * This value might be `undefined` if a query results in one or more errors (depending on the query's `errorPolicy`). + * + * @docGroup 1. Operation data + */ + data: unknown; + /** + * An object containing the result from the most recent _previous_ execution of this query. + * + * This value is `undefined` if this is the query's first execution. + * + * @docGroup 1. Operation data + */ + previousData: unknown; + /** + * If the query produces one or more errors, this object contains either an array of `graphQLErrors` or a single `networkError`. Otherwise, this value is `undefined`. + * + * For more information, see [Handling operation errors](https://www.apollographql.com/docs/react/data/error-handling/). + * + * @docGroup 1. Operation data + */ + error: unknown; + /** + * If `true`, the query is still in flight and results have not yet been returned. + * + * @docGroup 2. Network info + */ + loading: unknown; + /** + * A number indicating the current network state of the query's associated request. [See possible values.](https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/core/networkStatus.ts#L4) + * + * Used in conjunction with the [`notifyOnNetworkStatusChange`](#notifyonnetworkstatuschange) option. + * + * @docGroup 2. Network info + */ + networkStatus: unknown; + /** + * If `true`, the associated lazy query has been executed. + * + * This field is only present on the result object returned by [`useLazyQuery`](/react/data/queries/#executing-queries-manually). + * + * @docGroup 2. Network info + */ + called: unknown; + /** + * An object containing the variables that were provided for the query. + * + * @docGroup 1. Operation data + */ + variables: unknown; + + /** + * A function that enables you to re-execute the query, optionally passing in new `variables`. + * + * To guarantee that the refetch performs a network request, its `fetchPolicy` is set to `network-only` (unless the original query's `fetchPolicy` is `no-cache` or `cache-and-network`, which also guarantee a network request). + * + * See also [Refetching](https://www.apollographql.com/docs/react/data/queries/#refetching). + * + * @docGroup 3. Helper functions + */ + refetch: unknown; + /** + * {@inheritDoc @apollo/client!ObservableQuery#fetchMore:member(1)} + * + * @docGroup 3. Helper functions + */ + fetchMore: unknown; + /** + * {@inheritDoc @apollo/client!ObservableQuery#startPolling:member(1)} + * + * @docGroup 3. Helper functions + */ + startPolling: unknown; + /** + * {@inheritDoc @apollo/client!ObservableQuery#stopPolling:member(1)} + * + * @docGroup 3. Helper functions + */ + stopPolling: unknown; + /** + * {@inheritDoc @apollo/client!ObservableQuery#subscribeToMore:member(1)} + * + * @docGroup 3. Helper functions + */ + subscribeToMore: unknown; + /** + * {@inheritDoc @apollo/client!ObservableQuery#updateQuery:member(1)} + * + * @docGroup 3. Helper functions + */ + updateQuery: unknown; +} + +export interface MutationOptionsDocumentation { + /** + * A GraphQL document, often created with `gql` from the `graphql-tag` + * package, that contains a single mutation inside of it. + * + * @docGroup 1. Operation options + */ + mutation: unknown; + + /** + * Provide `no-cache` if the mutation's result should _not_ be written to the Apollo Client cache. + * + * The default value is `network-only` (which means the result _is_ written to the cache). + * + * Unlike queries, mutations _do not_ support [fetch policies](https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy) besides `network-only` and `no-cache`. + * + * @docGroup 3. Caching options + */ + fetchPolicy: unknown; + + /** + * To avoid retaining sensitive information from mutation root field + * arguments, Apollo Client v3.4+ automatically clears any `ROOT_MUTATION` + * fields from the cache after each mutation finishes. If you need this + * information to remain in the cache, you can prevent the removal by passing + * `keepRootFields: true` to the mutation. `ROOT_MUTATION` result data are + * also passed to the mutation `update` function, so we recommend obtaining + * the results that way, rather than using this option, if possible. + */ + keepRootFields: unknown; + + /** + * By providing either an object or a callback function that, when invoked after + * a mutation, allows you to return optimistic data and optionally skip updates + * via the `IGNORE` sentinel object, Apollo Client caches this temporary + * (and potentially incorrect) response until the mutation completes, enabling + * more responsive UI updates. + * + * For more information, see [Optimistic mutation results](https://www.apollographql.com/docs/react/performance/optimistic-ui/). + * + * @docGroup 3. Caching options + */ + optimisticResponse: unknown; + + /** + * A {@link MutationQueryReducersMap}, which is map from query names to + * mutation query reducers. Briefly, this map defines how to incorporate the + * results of the mutation into the results of queries that are currently + * being watched by your application. + */ + updateQueries: unknown; + + /** + * An array (or a function that _returns_ an array) that specifies which queries you want to refetch after the mutation occurs. + * + * Each array value can be either: + * + * - An object containing the `query` to execute, along with any `variables` + * + * - A string indicating the operation name of the query to refetch + * + * @docGroup 1. Operation options + */ + refetchQueries: unknown; + + /** + * If `true`, makes sure all queries included in `refetchQueries` are completed before the mutation is considered complete. + * + * The default value is `false` (queries are refetched asynchronously). + * + * @docGroup 1. Operation options + */ + awaitRefetchQueries: unknown; + + /** + * A function used to update the Apollo Client cache after the mutation completes. + * + * For more information, see [Updating the cache after a mutation](https://www.apollographql.com/docs/react/data/mutations#updating-the-cache-after-a-mutation). + * + * @docGroup 3. Caching options + */ + update: unknown; + + /** + * Optional callback for intercepting queries whose cache data has been updated by the mutation, as well as any queries specified in the `refetchQueries: [...]` list passed to `client.mutate`. + * + * Returning a `Promise` from `onQueryUpdated` will cause the final mutation `Promise` to await the returned `Promise`. Returning `false` causes the query to be ignored. + * + * @docGroup 1. Operation options + */ + onQueryUpdated: unknown; + + /** + * Specifies how the mutation handles a response that returns both GraphQL errors and partial results. + * + * For details, see [GraphQL error policies](https://www.apollographql.com/docs/react/data/error-handling/#graphql-error-policies). + * + * The default value is `none`, meaning that the mutation result includes error details but _not_ partial results. + * + * @docGroup 1. Operation options + */ + errorPolicy: unknown; + + /** + * An object containing all of the GraphQL variables your mutation requires to execute. + * + * Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. + * + * @docGroup 1. Operation options + */ + variables: unknown; + + /** + * If you're using [Apollo Link](https://www.apollographql.com/docs/react/api/link/introduction/), this object is the initial value of the `context` object that's passed along your link chain. + * + * @docGroup 2. Networking options + */ + context: unknown; + + /** + * The instance of `ApolloClient` to use to execute the mutation. + * + * By default, the instance that's passed down via context is used, but you can provide a different instance here. + * + * @docGroup 2. Networking options + */ + client: unknown; + /** + * If `true`, the in-progress mutation's associated component re-renders whenever the network status changes or a network error occurs. + * + * The default value is `false`. + * + * @docGroup 2. Networking options + */ + notifyOnNetworkStatusChange: unknown; + /** + * A callback function that's called when your mutation successfully completes with zero errors (or if `errorPolicy` is `ignore` and partial data is returned). + * + * This function is passed the mutation's result `data` and any options passed to the mutation. + * + * @docGroup 1. Operation options + */ + onCompleted: unknown; + /** + * A callback function that's called when the mutation encounters one or more errors (unless `errorPolicy` is `ignore`). + * + * This function is passed an [`ApolloError`](https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/errors/index.ts#L36-L39) object that contains either a `networkError` object or a `graphQLErrors` array, depending on the error(s) that occurred, as well as any options passed the mutation. + * + * @docGroup 1. Operation options + */ + onError: unknown; + /** + * If `true`, the mutation's `data` property is not updated with the mutation's result. + * + * The default value is `false`. + * + * @docGroup 1. Operation options + */ + ignoreResults: unknown; +} + +export interface MutationResultDocumentation { + /** + * The data returned from your mutation. Can be `undefined` if `ignoreResults` is `true`. + */ + data: unknown; + /** + * If the mutation produces one or more errors, this object contains either an array of `graphQLErrors` or a single `networkError`. Otherwise, this value is `undefined`. + * + * For more information, see [Handling operation errors](https://www.apollographql.com/docs/react/data/error-handling/). + */ + error: unknown; + /** + * If `true`, the mutation is currently in flight. + */ + loading: unknown; + /** + * If `true`, the mutation's mutate function has been called. + */ + called: unknown; + /** + * The instance of Apollo Client that executed the mutation. + * + * Can be useful for manually executing followup operations or writing data to the cache. + */ + client: unknown; + /** + * A function that you can call to reset the mutation's result to its initial, uncalled state. + */ + reset: unknown; +} + +export interface SubscriptionOptionsDocumentation { + /** + * A GraphQL document, often created with `gql` from the `graphql-tag` + * package, that contains a single subscription inside of it. + */ + query: unknown; + /** + * An object containing all of the variables your subscription needs to execute + */ + variables: unknown; + + /** + * Specifies the {@link ErrorPolicy} to be used for this operation + */ + errorPolicy: unknown; + + /** + * How you want your component to interact with the Apollo cache. For details, see [Setting a fetch policy](https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy). + */ + fetchPolicy: unknown; + + /** + * Determines if your subscription should be unsubscribed and subscribed again when an input to the hook (such as `subscription` or `variables`) changes. + */ + shouldResubscribe: unknown; + + /** + * An `ApolloClient` instance. By default `useSubscription` / `Subscription` uses the client passed down via context, but a different client can be passed in. + */ + client: unknown; + + /** + * Determines if the current subscription should be skipped. Useful if, for example, variables depend on previous queries and are not ready yet. + */ + skip: unknown; + + /** + * Shared context between your component and your network interface (Apollo Link). + */ + context: unknown; + + /** + * Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component completes the subscription. + * + * @since 3.7.0 + */ + onComplete: unknown; + + /** + * Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives data. The callback `options` object param consists of the current Apollo Client instance in `client`, and the received subscription data in `data`. + * + * @since 3.7.0 + */ + onData: unknown; + + /** + * Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives data. The callback `options` object param consists of the current Apollo Client instance in `client`, and the received subscription data in `subscriptionData`. + * + * @deprecated Use `onData` instead + */ + onSubscriptionData: unknown; + + /** + * Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives an error. + * + * @since 3.7.0 + */ + onError: unknown; + + /** + * Allows the registration of a callback function that will be triggered when the `useSubscription` Hook / `Subscription` component completes the subscription. + * + * @deprecated Use `onComplete` instead + */ + onSubscriptionComplete: unknown; +} + +export interface SubscriptionResultDocumentation { + /** + * A boolean that indicates whether any initial data has been returned + */ + loading: unknown; + /** + * An object containing the result of your GraphQL subscription. Defaults to an empty object. + */ + data: unknown; + /** + * A runtime error with `graphQLErrors` and `networkError` properties + */ + error: unknown; +} diff --git a/src/react/types/types.ts b/src/react/types/types.ts index f6e24f33474..785c2ed793b 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -13,16 +13,22 @@ import type { ApolloClient, DefaultContext, FetchPolicy, - MutationOptions, NetworkStatus, ObservableQuery, OperationVariables, InternalRefetchQueriesInclude, WatchQueryOptions, WatchQueryFetchPolicy, + SubscribeToMoreOptions, + ApolloQueryResult, + FetchMoreQueryOptions, ErrorPolicy, RefetchWritePolicy, } from "../../core/index.js"; +import type { + MutationSharedOptions, + SharedWatchQueryOptions, +} from "../../core/watchQueryOptions.js"; /* QueryReference type */ @@ -40,18 +46,25 @@ export type CommonOptions = TOptions & { export interface BaseQueryOptions< TVariables extends OperationVariables = OperationVariables, -> extends Omit, "query"> { + TData = any, +> extends SharedWatchQueryOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#ssr:member} */ ssr?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#client:member} */ client?: ApolloClient; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; } export interface QueryFunctionOptions< TData = any, TVariables extends OperationVariables = OperationVariables, -> extends BaseQueryOptions { +> extends BaseQueryOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#skip:member} */ skip?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} */ onCompleted?: (data: TData) => void; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} */ onError?: (error: ApolloError) => void; // Default WatchQueryOptions for this useQuery, providing initial values for @@ -59,35 +72,81 @@ export interface QueryFunctionOptions< // by option, not whole), but never overriding options previously passed to // useQuery (or options added/modified later by other means). // TODO What about about default values that are expensive to evaluate? + /** @internal */ defaultOptions?: Partial>; } -export type ObservableQueryFields< +export interface ObservableQueryFields< TData, TVariables extends OperationVariables, -> = Pick< - ObservableQuery, - | "startPolling" - | "stopPolling" - | "subscribeToMore" - | "updateQuery" - | "refetch" - | "reobserve" - | "variables" - | "fetchMore" ->; +> { + /** {@inheritDoc @apollo/client!QueryResultDocumentation#startPolling:member} */ + startPolling(pollInterval: number): void; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#stopPolling:member} */ + stopPolling(): void; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#subscribeToMore:member} */ + subscribeToMore< + TSubscriptionData = TData, + TSubscriptionVariables extends OperationVariables = TVariables, + >( + options: SubscribeToMoreOptions< + TData, + TSubscriptionVariables, + TSubscriptionData + > + ): () => void; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#updateQuery:member} */ + updateQuery( + mapFn: ( + previousQueryResult: TData, + options: Pick, "variables"> + ) => TData + ): void; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#refetch:member} */ + refetch(variables?: Partial): Promise>; + /** @internal */ + reobserve( + newOptions?: Partial>, + newNetworkStatus?: NetworkStatus + ): Promise>; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#variables:member} */ + variables: TVariables | undefined; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#fetchMore:member} */ + fetchMore< + TFetchData = TData, + TFetchVars extends OperationVariables = TVariables, + >( + fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: ( + previousQueryResult: TData, + options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + } + ) => TData; + } + ): Promise>; +} export interface QueryResult< TData = any, TVariables extends OperationVariables = OperationVariables, > extends ObservableQueryFields { + /** {@inheritDoc @apollo/client!QueryResultDocumentation#client:member} */ client: ApolloClient; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#observable:member} */ observable: ObservableQuery; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#data:member} */ data: TData | undefined; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#previousData:member} */ previousData?: TData; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#error:member} */ error?: ApolloError; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#loading:member} */ loading: boolean; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#networkStatus:member} */ networkStatus: NetworkStatus; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#called:member} */ called: boolean; } @@ -96,6 +155,7 @@ export interface QueryDataOptions< TVariables extends OperationVariables = OperationVariables, > extends QueryFunctionOptions { children?: (result: QueryResult) => ReactTypes.ReactNode; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ query: DocumentNode | TypedDocumentNode; } @@ -107,8 +167,15 @@ export interface QueryHookOptions< export interface LazyQueryHookOptions< TData = any, TVariables extends OperationVariables = OperationVariables, -> extends Omit, "skip"> {} +> extends BaseQueryOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} */ + onCompleted?: (data: TData) => void; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} */ + onError?: (error: ApolloError) => void; + /** @internal */ + defaultOptions?: Partial>; +} export interface LazyQueryHookExecOptions< TData = any, TVariables extends OperationVariables = OperationVariables, @@ -124,24 +191,28 @@ export type SuspenseQueryHookFetchPolicy = Extract< export interface SuspenseQueryHookOptions< TData = unknown, TVariables extends OperationVariables = OperationVariables, -> extends Pick< - QueryHookOptions, - | "client" - | "variables" - | "errorPolicy" - | "context" - | "canonizeResults" - | "returnPartialData" - | "refetchWritePolicy" - > { +> { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#client:member} */ + client?: ApolloClient; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ + context?: DefaultContext; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables?: TVariables; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ + errorPolicy?: ErrorPolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ + canonizeResults?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#returnPartialData:member} */ + returnPartialData?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#refetchWritePolicy_suspense:member} */ + refetchWritePolicy?: RefetchWritePolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: SuspenseQueryHookFetchPolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#queryKey:member} */ queryKey?: string | number | any[]; /** - * If `true`, the query is not executed. The default value is `false`. - * - * @deprecated We recommend using `skipToken` in place of the `skip` option as - * it is more type-safe. + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#skip_deprecated:member} * * @example Recommended usage of `skipToken`: * ```ts @@ -175,10 +246,7 @@ export interface BackgroundQueryHookOptions< queryKey?: string | number | any[]; /** - * If `true`, the query is not executed. The default value is `false`. - * - * @deprecated We recommend using `skipToken` in place of the `skip` option as - * it is more type-safe. + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#skip_deprecated:member} * * @example Recommended usage of `skipToken`: * ```ts @@ -196,77 +264,36 @@ export type LoadableQueryHookFetchPolicy = Extract< >; export interface LoadableQueryHookOptions { - /** - * @deprecated - * Using `canonizeResults` can result in memory leaks so we generally do not - * recommend using this option anymore. - * A future version of Apollo Client will contain a similar feature without - * the risk of memory leaks. - * - * Whether to canonize cache results before returning them. Canonization - * takes some extra time, but it speeds up future deep equality comparisons. - * Defaults to false. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ canonizeResults?: boolean; - /** - * The instance of {@link ApolloClient} to use to execute the query. - * - * By default, the instance that's passed down via context is used, but you - * can provide a different instance here. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#client:member} */ client?: ApolloClient; - /** - * Context to be passed to link execution chain - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; - /** - * Specifies the {@link ErrorPolicy} to be used for this query - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** - * - * Specifies how the query interacts with the Apollo Client cache during - * execution (for example, whether it checks the cache for results before - * sending a request to the server). - * - * For details, see {@link https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy | Setting a fetch policy}. - * - * The default value is `cache-first`. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: LoadableQueryHookFetchPolicy; - /** - * A unique identifier for the query. Each item in the array must be a stable - * identifier to prevent infinite fetches. - * - * This is useful when using the same query and variables combination in more - * than one component, otherwise the components may clobber each other. This - * can also be used to force the query to re-evaluate fresh. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#queryKey:member} */ queryKey?: string | number | any[]; - /** - * Specifies whether a {@link NetworkStatus.refetch} operation should merge - * incoming field data with existing data, or overwrite the existing data. - * Overwriting is probably preferable, but merging is currently the default - * behavior, for backwards compatibility with Apollo Client 3.x. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#refetchWritePolicy:member} */ refetchWritePolicy?: RefetchWritePolicy; - /** - * Allow returning incomplete data from the cache when a larger query cannot - * be fully satisfied by the cache, instead of returning nothing. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#returnPartialData:member} */ returnPartialData?: boolean; } /** - * @deprecated TODO Delete this unused interface. + * @deprecated This type will be removed in the next major version of Apollo Client */ export interface QueryLazyOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables?: TVariables; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; } /** - * @deprecated TODO Delete this unused type alias. + * @deprecated This type will be removed in the next major version of Apollo Client */ export type LazyQueryResult< TData, @@ -274,7 +301,7 @@ export type LazyQueryResult< > = QueryResult; /** - * @deprecated TODO Delete this unused type alias. + * @deprecated This type will be removed in the next major version of Apollo Client */ export type QueryTuple< TData, @@ -291,7 +318,10 @@ export type LazyQueryExecFunction< export type LazyQueryResultTuple< TData, TVariables extends OperationVariables, -> = [LazyQueryExecFunction, QueryResult]; +> = [ + execute: LazyQueryExecFunction, + result: QueryResult, +]; /* Mutation types */ @@ -304,14 +334,16 @@ export interface BaseMutationOptions< TVariables = OperationVariables, TContext = DefaultContext, TCache extends ApolloCache = ApolloCache, -> extends Omit< - MutationOptions, - "mutation" - > { +> extends MutationSharedOptions { + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#client:member} */ client?: ApolloClient; + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#notifyOnNetworkStatusChange:member} */ notifyOnNetworkStatusChange?: boolean; + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#onCompleted:member} */ onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#onError:member} */ onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#ignoreResults:member} */ ignoreResults?: boolean; } @@ -321,15 +353,22 @@ export interface MutationFunctionOptions< TContext = DefaultContext, TCache extends ApolloCache = ApolloCache, > extends BaseMutationOptions { + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#mutation:member} */ mutation?: DocumentNode | TypedDocumentNode; } export interface MutationResult { + /** {@inheritDoc @apollo/client!MutationResultDocumentation#data:member} */ data?: TData | null; + /** {@inheritDoc @apollo/client!MutationResultDocumentation#error:member} */ error?: ApolloError; + /** {@inheritDoc @apollo/client!MutationResultDocumentation#loading:member} */ loading: boolean; + /** {@inheritDoc @apollo/client!MutationResultDocumentation#called:member} */ called: boolean; + /** {@inheritDoc @apollo/client!MutationResultDocumentation#client:member} */ client: ApolloClient; + /** {@inheritDoc @apollo/client!MutationResultDocumentation#reset:member} */ reset(): void; } @@ -364,12 +403,12 @@ export type MutationTuple< TContext = DefaultContext, TCache extends ApolloCache = ApolloCache, > = [ - ( + mutate: ( options?: MutationFunctionOptions // TODO This FetchResult seems strange here, as opposed to an // ApolloQueryResult ) => Promise>, - MutationResult, + result: MutationResult, ]; /* Subscription types */ @@ -388,33 +427,44 @@ export interface BaseSubscriptionOptions< TData = any, TVariables extends OperationVariables = OperationVariables, > { + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#variables:member} */ variables?: TVariables; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: FetchPolicy; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#shouldResubscribe:member} */ shouldResubscribe?: | boolean | ((options: BaseSubscriptionOptions) => boolean); + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#client:member} */ client?: ApolloClient; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#skip:member} */ skip?: boolean; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#context:member} */ context?: DefaultContext; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onComplete:member} */ onComplete?: () => void; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onData:member} */ onData?: (options: OnDataOptions) => any; - /** - * @deprecated Use onData instead - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onSubscriptionData:member} */ onSubscriptionData?: (options: OnSubscriptionDataOptions) => any; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onError:member} */ onError?: (error: ApolloError) => void; - /** - * @deprecated Use onComplete instead - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onSubscriptionComplete:member} */ onSubscriptionComplete?: () => void; } export interface SubscriptionResult { + /** {@inheritDoc @apollo/client!SubscriptionResultDocumentation#loading:member} */ loading: boolean; + /** {@inheritDoc @apollo/client!SubscriptionResultDocumentation#data:member} */ data?: TData; + /** {@inheritDoc @apollo/client!SubscriptionResultDocumentation#error:member} */ error?: ApolloError; // This was added by the legacy useSubscription type, and is tested in unit // tests, but probably shouldn’t be added to the result. + /** + * @internal + */ variables?: TVariables; } diff --git a/src/utilities/graphql/DocumentTransform.ts b/src/utilities/graphql/DocumentTransform.ts index 732c1c7d1a6..ec6e0e44152 100644 --- a/src/utilities/graphql/DocumentTransform.ts +++ b/src/utilities/graphql/DocumentTransform.ts @@ -12,7 +12,19 @@ export type DocumentTransformCacheKey = ReadonlyArray; type TransformFn = (document: DocumentNode) => DocumentNode; interface DocumentTransformOptions { + /** + * Determines whether to cache the transformed GraphQL document. Caching can speed up repeated calls to the document transform for the same input document. Set to `false` to completely disable caching for the document transform. When disabled, this option takes precedence over the [`getCacheKey`](#getcachekey) option. + * + * The default value is `true`. + */ cache?: boolean; + /** + * Defines a custom cache key for a GraphQL document that will determine whether to re-run the document transform when given the same input GraphQL document. Returns an array that defines the cache key. Return `undefined` to disable caching for that GraphQL document. + * + * > **Note:** The items in the array may be any type, but also need to be referentially stable to guarantee a stable cache key. + * + * The default implementation of this function returns the `document` as the cache key. + */ getCacheKey?: ( document: DocumentNode ) => DocumentTransformCacheKey | undefined; diff --git a/tsdoc.json b/tsdoc.json new file mode 100644 index 00000000000..c49aafb4c6e --- /dev/null +++ b/tsdoc.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + + // Inherit the TSDoc configuration for API Extractor + "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"], + + "tagDefinitions": [ + { + "tagName": "@since", + "syntaxKind": "block", + "allowMultiple": false + }, + { + "tagName": "@docGroup", + "syntaxKind": "block", + "allowMultiple": false + } + ], + + "supportForTags": { + "@since": true, + "@docGroup": true + } +} From 2964a13c08e5644b8484a94ede12e02cdf85bce0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 26 Jan 2024 16:29:33 -0700 Subject: [PATCH 88/90] Document new React APIs in code - Part 2 of 2 (#11523) --- .api-reports/api-report-react.md | 16 ++--- .api-reports/api-report-react_hooks.md | 14 ++--- .api-reports/api-report-react_internal.md | 6 +- .api-reports/api-report.md | 16 ++--- .prettierignore | 2 + .../Overrides/UseLoadableQueryResult.js | 58 +++++++++++++++++++ docs/source/api/react/hooks.mdx | 11 ++++ docs/source/api/react/preloading.mdx | 11 ++++ docs/source/config.json | 1 + src/react/hooks/useLoadableQuery.ts | 54 ++++++++++++++++- src/react/hooks/useQueryRefHandlers.ts | 2 +- src/react/internal/cache/QueryReference.ts | 39 +++++++++++++ .../query-preloader/createQueryPreloader.ts | 7 ++- 13 files changed, 206 insertions(+), 31 deletions(-) create mode 100644 docs/shared/Overrides/UseLoadableQueryResult.js create mode 100644 docs/source/api/react/preloading.mdx diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index e7a4e16d51c..3489e1e54c3 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -547,7 +547,7 @@ type ConcastSourcesIterable = Iterable>; export interface Context extends Record { } -// @public +// @alpha export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; // @public (undocumented) @@ -1759,13 +1759,13 @@ interface QueryOptions { // // @public export interface QueryReference { - // (undocumented) + // @internal (undocumented) [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // - // (undocumented) + // @internal (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; - // (undocumented) + // @alpha toPromise(): Promise>; } @@ -2251,13 +2251,13 @@ export function useLoadableQuery, TVariables>; -// @public (undocumented) +// @public export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; // @public (undocumented) export type UseLoadableQueryResult = [ -LoadQueryFunction, -QueryReference | null, +loadQuery: LoadQueryFunction, +queryRef: QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -2398,7 +2398,7 @@ interface WatchQueryOptions { // // @public interface QueryReference { - // (undocumented) + // @internal (undocumented) [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // - // (undocumented) + // @internal (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; - // (undocumented) + // @alpha toPromise(): Promise>; } @@ -2091,13 +2091,13 @@ export function useLoadableQuery, TVariables>; -// @public (undocumented) +// @public export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; // @public (undocumented) export type UseLoadableQueryResult = [ -LoadQueryFunction, -QueryReference | null, +loadQuery: LoadQueryFunction, +queryRef: QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -2234,7 +2234,7 @@ interface WatchQueryOptions { // // @public export interface QueryReference { - // (undocumented) + // @internal (undocumented) [PROMISE_SYMBOL]: QueryRefPromise; - // (undocumented) + // @internal (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; - // (undocumented) + // @alpha toPromise(): Promise>; } diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 39d55033f88..9040bd40123 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -544,7 +544,7 @@ export const concat: typeof ApolloLink.concat; // @public (undocumented) export const createHttpLink: (linkOptions?: HttpOptions) => ApolloLink; -// @public +// @alpha export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; // @public @deprecated (undocumented) @@ -2323,13 +2323,13 @@ export { QueryOptions } // // @public export interface QueryReference { - // (undocumented) + // @internal (undocumented) [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // - // (undocumented) + // @internal (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; - // (undocumented) + // @alpha toPromise(): Promise>; } @@ -2904,13 +2904,13 @@ export function useLoadableQuery, TVariables>; -// @public (undocumented) +// @public export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; // @public (undocumented) export type UseLoadableQueryResult = [ -LoadQueryFunction, -QueryReference | null, +loadQuery: LoadQueryFunction, +queryRef: QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -3077,7 +3077,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:29:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useLoadableQuery.ts:49:5 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useLoadableQuery.ts:106:1 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.prettierignore b/.prettierignore index d1ab5b159ad..fe391b018fc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -29,6 +29,8 @@ /docs/shared/** !/docs/shared/ApiDoc !/docs/shared/ApiDoc/** +!/docs/shared/Overrides +!/docs/shared/Overrides/** node_modules/ .yalc/ diff --git a/docs/shared/Overrides/UseLoadableQueryResult.js b/docs/shared/Overrides/UseLoadableQueryResult.js new file mode 100644 index 00000000000..19a4d345d44 --- /dev/null +++ b/docs/shared/Overrides/UseLoadableQueryResult.js @@ -0,0 +1,58 @@ +import React from "react"; +import { useMDXComponents } from "@mdx-js/react"; +import { ManualTuple } from "../ApiDoc"; + +const HANDLERS = `{ + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; +}`; + +const RETURN_VALUE = `[ + loadQuery: LoadQueryFunction, + queryRef: QueryReference | null, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; + } +]`; + +export function UseLoadableQueryResult() { + const MDX = useMDXComponents(); + + return ( +
+ + {RETURN_VALUE} + + A tuple of three values: + ", + description: + "A function used to imperatively load a query. Calling this function will create or update the `queryRef` returned by `useLoadableQuery`, which should be passed to `useReadQuery`.", + }, + { + name: "queryRef", + type: "QueryReference | null", + description: + "The `queryRef` used by `useReadQuery` to read the query result.", + canonicalReference: "@apollo/client!QueryReference:interface", + }, + { + name: "handlers", + description: + "Additional handlers used for the query, such as `refetch`.", + type: HANDLERS, + }, + ]} + /> +
+ ); +} + +UseLoadableQueryResult.propTypes = {}; diff --git a/docs/source/api/react/hooks.mdx b/docs/source/api/react/hooks.mdx index c6e3249253b..80ef1dc3710 100644 --- a/docs/source/api/react/hooks.mdx +++ b/docs/source/api/react/hooks.mdx @@ -10,6 +10,8 @@ api_doc: - "@apollo/client!useSubscription:function(1)" - "@apollo/client!useApolloClient:function(1)" - "@apollo/client!useReactiveVar:function(1)" + - "@apollo/client!useLoadableQuery:function(5)" + - "@apollo/client!useQueryRefHandlers:function(1)" --- import UseFragmentOptions from '../../../shared/useFragment-options.mdx'; @@ -19,6 +21,7 @@ import UseSuspenseQueryResult from '../../../shared/useSuspenseQuery-result.mdx' import UseBackgroundQueryResult from '../../../shared/useBackgroundQuery-result.mdx'; import UseReadQueryResult from '../../../shared/useReadQuery-result.mdx'; import { FunctionDetails, PropertySignatureTable, ManualTuple, InterfaceDetails } from '../../../shared/ApiDoc'; +import { UseLoadableQueryResult } from '../../../shared/Overrides/UseLoadableQueryResult' ## The `ApolloProvider` component @@ -405,6 +408,14 @@ function useReadQuery( +} +/> + + + ## `skipToken` diff --git a/docs/source/api/react/preloading.mdx b/docs/source/api/react/preloading.mdx new file mode 100644 index 00000000000..644fe13d122 --- /dev/null +++ b/docs/source/api/react/preloading.mdx @@ -0,0 +1,11 @@ +--- +title: Preloading +description: Apollo Client preloading API reference +minVersion: 3.9.0 +api_doc: + - "@apollo/client!createQueryPreloader:function(1)" +--- + +import { FunctionDetails } from '../../../shared/ApiDoc'; + + diff --git a/docs/source/config.json b/docs/source/config.json index 60b6f0b1b6e..98c46b99f90 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -80,6 +80,7 @@ }, "React": { "Hooks": "/api/react/hooks", + "Preloading": "/api/react/preloading", "Testing": "/api/react/testing", "SSR": "/api/react/ssr", "Components (deprecated)": "/api/react/components", diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 796721a92fc..7c0c0cca4e6 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -41,11 +41,16 @@ export type UseLoadableQueryResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, > = [ - LoadQueryFunction, - QueryReference | null, + loadQuery: LoadQueryFunction, + queryRef: QueryReference | null, { + /** {@inheritDoc @apollo/client!QueryResultDocumentation#fetchMore:member} */ fetchMore: FetchMoreFunction; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#refetch:member} */ refetch: RefetchFunction; + /** + * A function that resets the `queryRef` back to `null`. + */ reset: ResetFunction; }, ]; @@ -98,6 +103,51 @@ export function useLoadableQuery< } ): UseLoadableQueryResult, TVariables>; +/** + * A hook for imperatively loading a query, such as responding to a user + * interaction. + * + * > Refer to the [Suspense - Fetching in response to user interaction](https://www.apollographql.com/docs/react/data/suspense#fetching-in-response-to-user-interaction) section for a more in-depth overview of `useLoadableQuery`. + * + * @example + * ```jsx + * import { gql, useLoadableQuery } from "@apollo/client"; + * + * const GET_GREETING = gql` + * query GetGreeting($language: String!) { + * greeting(language: $language) { + * message + * } + * } + * `; + * + * function App() { + * const [loadGreeting, queryRef] = useLoadableQuery(GET_GREETING); + * + * return ( + * <> + * + * Loading...}> + * {queryRef && } + * + * + * ); + * } + * + * function Hello({ queryRef }) { + * const { data } = useReadQuery(queryRef); + * + * return
{data.greeting.message}
; + * } + * ``` + * + * @since 3.9.0 + * @param query - A GraphQL query document parsed into an AST by `gql`. + * @param options - Options to control how the query is executed. + * @returns A tuple in the form of `[loadQuery, queryRef, handlers]` + */ export function useLoadableQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables, diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts index c5470e1540f..b0422afa678 100644 --- a/src/react/hooks/useQueryRefHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -36,7 +36,7 @@ export interface UseQueryRefHandlersResult< * // ... * } * ``` - * + * @since 3.9.0 * @param queryRef - A `QueryReference` returned from `useBackgroundQuery`, `useLoadableQuery`, or `createQueryPreloader`. */ export function useQueryRefHandlers< diff --git a/src/react/internal/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts index 7328a0533c3..dc26adf541c 100644 --- a/src/react/internal/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -35,8 +35,47 @@ const PROMISE_SYMBOL: unique symbol = Symbol(); * suspend until the promise resolves. */ export interface QueryReference { + /** @internal */ readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + /** @internal */ [PROMISE_SYMBOL]: QueryRefPromise; + /** + * A function that returns a promise that resolves when the query has finished + * loading. The promise resolves with the `QueryReference` itself. + * + * @remarks + * This method is useful for preloading queries in data loading routers, such + * as [React Router](https://reactrouter.com/en/main) or [TanStack Router](https://tanstack.com/router), + * to prevent routes from transitioning until the query has finished loading. + * `data` is not exposed on the promise to discourage using the data in + * `loader` functions and exposing it to your route components. Instead, we + * prefer you rely on `useReadQuery` to access the data to ensure your + * component can rerender with cache updates. If you need to access raw query + * data, use `client.query()` directly. + * + * @example + * Here's an example using React Router's `loader` function: + * ```ts + * import { createQueryPreloader } from "@apollo/client"; + * + * const preloadQuery = createQueryPreloader(client); + * + * export async function loader() { + * const queryRef = preloadQuery(GET_DOGS_QUERY); + * + * return queryRef.toPromise(); + * } + * + * export function RouteComponent() { + * const queryRef = useLoaderData(); + * const { data } = useReadQuery(queryRef); + * + * // ... + * } + * ``` + * + * @alpha + */ toPromise(): Promise>; } diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 12b7efd7983..b7a9d22afcb 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -153,7 +153,9 @@ export interface PreloadQueryFunction { * when you want to start loading a query as early as possible outside of a * React component. * - * @param client - The ApolloClient instance that will be used to load queries + * > Refer to the [Suspense - Initiating queries outside React](https://www.apollographql.com/docs/react/data/suspense#initiating-queries-outside-react) section for a more in-depth overview. + * + * @param client - The `ApolloClient` instance that will be used to load queries * from the returned `preloadQuery` function. * @returns The `preloadQuery` function. * @@ -161,7 +163,8 @@ export interface PreloadQueryFunction { * ```js * const preloadQuery = createQueryPreloader(client); * ``` - * @experimental + * @since 3.9.0 + * @alpha */ export function createQueryPreloader( client: ApolloClient From deffc69e93aafe7e6a30a03b105dfece7a36ad24 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 26 Jan 2024 16:42:46 -0700 Subject: [PATCH 89/90] Add docs component for API members that are marked as alpha/beta (#11529) --- docs/shared/ApiDoc/DocBlock.js | 29 ++++++++++++++++++++ docs/shared/ApiDoc/Function.js | 7 ++++- docs/shared/ApiDoc/PropertySignatureTable.js | 2 +- docs/shared/ApiDoc/index.js | 9 +++++- 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/docs/shared/ApiDoc/DocBlock.js b/docs/shared/ApiDoc/DocBlock.js index 18e29d26d19..28ee46b305a 100644 --- a/docs/shared/ApiDoc/DocBlock.js +++ b/docs/shared/ApiDoc/DocBlock.js @@ -11,10 +11,12 @@ export function DocBlock({ example = false, remarksCollapsible = false, deprecated = false, + releaseTag = false, }) { return ( {deprecated && } + {releaseTag && } {summary && } {remarks && ( + This is in{" "} + + {item.releaseTag.toLowerCase()} stage + {" "} + and is subject to breaking changes. + + ); +} + +ReleaseTag.propTypes = { + canonicalReference: PropTypes.string.isRequired, +}; diff --git a/docs/shared/ApiDoc/Function.js b/docs/shared/ApiDoc/Function.js index 7cf0e8cbc3d..aad711fe8f7 100644 --- a/docs/shared/ApiDoc/Function.js +++ b/docs/shared/ApiDoc/Function.js @@ -109,7 +109,12 @@ export function FunctionDetails({ headingLevel={headingLevel} since /> - + {item.comment?.examples.length == 0 ? null : ( <> diff --git a/docs/shared/ApiDoc/index.js b/docs/shared/ApiDoc/index.js index 4173bdebe62..06728b10116 100644 --- a/docs/shared/ApiDoc/index.js +++ b/docs/shared/ApiDoc/index.js @@ -1,5 +1,12 @@ export { useApiDocContext } from "./Context"; -export { DocBlock, Deprecated, Example, Remarks, Summary } from "./DocBlock"; +export { + DocBlock, + Deprecated, + Example, + Remarks, + ReleaseTag, + Summary, +} from "./DocBlock"; export { PropertySignatureTable } from "./PropertySignatureTable"; export { ApiDocHeading, SubHeading, SectionHeading } from "./Heading"; export { InterfaceDetails } from "./InterfaceDetails"; From 2c836af2af6e5a623189d194cebda5ba01f9d115 Mon Sep 17 00:00:00 2001 From: Alessia Bellisario Date: Tue, 30 Jan 2024 14:06:51 -0500 Subject: [PATCH 90/90] Exit prerelease --- .changeset/pre.json | 45 --------------------------------------------- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 3 insertions(+), 48 deletions(-) delete mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 01678933c4d..00000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "mode": "pre", - "tag": "rc", - "initialVersions": { - "@apollo/client": "3.8.3" - }, - "changesets": [ - "beige-geese-wink", - "breezy-spiders-tap", - "chatty-comics-yawn", - "clean-items-smash", - "cold-llamas-turn", - "curvy-seas-hope", - "dirty-kids-crash", - "dirty-tigers-matter", - "forty-cups-shop", - "friendly-clouds-laugh", - "hot-ducks-burn", - "late-rabbits-protect", - "mighty-coats-check", - "pink-apricots-yawn", - "polite-avocados-warn", - "quick-hats-marry", - "rare-snakes-melt", - "shaggy-ears-scream", - "shaggy-sheep-pull", - "six-rocks-arrive", - "sixty-boxes-rest", - "smooth-plums-shout", - "sour-sheep-walk", - "spicy-drinks-camp", - "strong-terms-perform", - "swift-zoos-collect", - "thick-mice-collect", - "thick-tips-cry", - "thirty-ties-arrive", - "tough-timers-begin", - "unlucky-rats-decide", - "violet-lions-draw", - "wet-forks-rhyme", - "wild-dolphins-jog", - "wise-news-grab", - "yellow-flies-repeat" - ] -} diff --git a/package-lock.json b/package-lock.json index 213c4259e85..e77230ea76b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.0-rc.1", + "version": "3.8.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.0-rc.1", + "version": "3.8.10", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index b5d8b9db27c..458bd6d5b28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.0-rc.1", + "version": "3.8.10", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [