From 8dfc90db522d5c0c09ed4fdb20551a616f33823d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 6 Dec 2024 16:18:12 +0000 Subject: [PATCH 01/68] Enter prerelease mode --- .changeset/pre.json | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 00000000000..461875c7499 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,8 @@ +{ + "mode": "pre", + "tag": "alpha", + "initialVersions": { + "@apollo/client": "3.12.2" + }, + "changesets": [] +} From ba5cc330f8734a989eef71e883861f848388ac0c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 6 Dec 2024 09:48:46 -0700 Subject: [PATCH 02/68] Don't call `onError` if errors are thrown in `onCompleted` (#12174) --- .changeset/fluffy-worms-fail.md | 5 + .changeset/sharp-windows-switch.md | 5 + .size-limits.json | 2 +- .../hooks/__tests__/useMutation.test.tsx | 55 +++++++ src/react/hooks/useMutation.ts | 137 +++++++++--------- 5 files changed, 137 insertions(+), 67 deletions(-) create mode 100644 .changeset/fluffy-worms-fail.md create mode 100644 .changeset/sharp-windows-switch.md diff --git a/.changeset/fluffy-worms-fail.md b/.changeset/fluffy-worms-fail.md new file mode 100644 index 00000000000..b232d671f0e --- /dev/null +++ b/.changeset/fluffy-worms-fail.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Ensure errors thrown in the `onCompleted` callback from `useMutation` don't call `onError`. diff --git a/.changeset/sharp-windows-switch.md b/.changeset/sharp-windows-switch.md new file mode 100644 index 00000000000..7b47f0c978b --- /dev/null +++ b/.changeset/sharp-windows-switch.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Reject the mutation promise if errors are thrown in the `onCompleted` callback of `useMutation`. diff --git a/.size-limits.json b/.size-limits.json index c7b4947027f..54bce409ebd 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41615, + "dist/apollo-client.min.cjs": 41613, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34349 } diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx index 8e81130201f..8ac66118371 100644 --- a/src/react/hooks/__tests__/useMutation.test.tsx +++ b/src/react/hooks/__tests__/useMutation.test.tsx @@ -1207,6 +1207,61 @@ describe("useMutation Hook", () => { expect.objectContaining({ variables }) ); }); + + // https://github.com/apollographql/apollo-client/issues/12008 + it("does not call onError if errors are thrown in the onCompleted callback", async () => { + const CREATE_TODO_DATA = { + createTodo: { + id: 1, + priority: "Low", + description: "Get milk!", + __typename: "Todo", + }, + }; + + const variables = { + priority: "Low", + description: "Get milk2.", + }; + + const mocks = [ + { + request: { + query: CREATE_TODO_MUTATION, + variables, + }, + result: { + data: CREATE_TODO_DATA, + }, + }, + ]; + + const onError = jest.fn(); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useMutation(CREATE_TODO_MUTATION, { + onCompleted: () => { + throw new Error("Oops"); + }, + onError, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + const [createTodo] = await takeSnapshot(); + + await expect(createTodo({ variables })).rejects.toEqual( + new Error("Oops") + ); + + expect(onError).not.toHaveBeenCalled(); + }); }); describe("ROOT_MUTATION cache data", () => { diff --git a/src/react/hooks/useMutation.ts b/src/react/hooks/useMutation.ts index ea0f47ddc6c..1f3f8ce29a4 100644 --- a/src/react/hooks/useMutation.ts +++ b/src/react/hooks/useMutation.ts @@ -138,82 +138,87 @@ export function useMutation< return client .mutate(clientOptions as MutationOptions) - .then((response) => { - const { data, errors } = response; - const error = - errors && errors.length > 0 ? - new ApolloError({ graphQLErrors: errors }) - : void 0; - - const onError = - executeOptions.onError || ref.current.options?.onError; - - if (error && onError) { - onError( - error, - clientOptions as MutationOptions - ); - } + .then( + (response) => { + const { data, errors } = response; + const error = + errors && errors.length > 0 ? + new ApolloError({ graphQLErrors: errors }) + : void 0; + + const onError = + executeOptions.onError || ref.current.options?.onError; + + if (error && onError) { + onError( + error, + clientOptions as MutationOptions + ); + } - if ( - mutationId === ref.current.mutationId && - !clientOptions.ignoreResults - ) { - const result = { - called: true, - loading: false, - data, - error, - client, - }; - - if (ref.current.isMounted && !equal(ref.current.result, result)) { - setResult((ref.current.result = result)); + if ( + mutationId === ref.current.mutationId && + !clientOptions.ignoreResults + ) { + const result = { + called: true, + loading: false, + data, + error, + client, + }; + + if (ref.current.isMounted && !equal(ref.current.result, result)) { + setResult((ref.current.result = result)); + } } - } - const onCompleted = - executeOptions.onCompleted || ref.current.options?.onCompleted; + const onCompleted = + executeOptions.onCompleted || ref.current.options?.onCompleted; - if (!error) { - onCompleted?.( - response.data!, - clientOptions as MutationOptions - ); - } + if (!error) { + onCompleted?.( + response.data!, + clientOptions as MutationOptions + ); + } - return response; - }) - .catch((error) => { - if (mutationId === ref.current.mutationId && ref.current.isMounted) { - const result = { - loading: false, - error, - data: void 0, - called: true, - client, - }; - - if (!equal(ref.current.result, result)) { - setResult((ref.current.result = result)); + return response; + }, + (error) => { + if ( + mutationId === ref.current.mutationId && + ref.current.isMounted + ) { + const result = { + loading: false, + error, + data: void 0, + called: true, + client, + }; + + if (!equal(ref.current.result, result)) { + setResult((ref.current.result = result)); + } } - } - const onError = - executeOptions.onError || ref.current.options?.onError; + const onError = + executeOptions.onError || ref.current.options?.onError; - if (onError) { - onError( - error, - clientOptions as MutationOptions - ); + if (onError) { + onError( + error, + clientOptions as MutationOptions + ); - // TODO(brian): why are we returning this here??? - return { data: void 0, errors: error }; - } + // TODO(brian): why are we returning this here??? + return { data: void 0, errors: error }; + } - throw error; - }); + throw error; + } + ); }, [] ); From 2422df202a7ec71365d5a8ab5b3b554fcf60e4af Mon Sep 17 00:00:00 2001 From: "Michael \"Mike\" Ferris" Date: Wed, 22 Jan 2025 16:25:29 -0500 Subject: [PATCH 03/68] Deprecate ignoreResults in useMutation (#12296) --- .changeset/khaki-cars-develop.md | 5 +++++ src/react/types/types.ts | 5 ++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 .changeset/khaki-cars-develop.md diff --git a/.changeset/khaki-cars-develop.md b/.changeset/khaki-cars-develop.md new file mode 100644 index 00000000000..7f02084721f --- /dev/null +++ b/.changeset/khaki-cars-develop.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Deprecate option `ignoreResults` in `useMutation`. diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 7812fb34bc2..3f8160bdfb5 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -358,7 +358,10 @@ export interface BaseMutationOptions< ) => void; /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#onError:member} */ onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; - /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#ignoreResults:member} */ + /** + * {@inheritDoc @apollo/client!MutationOptionsDocumentation#ignoreResults:member} + * @deprecated This property will be removed in the next major version of Apollo Client + */ ignoreResults?: boolean; } From 9849694717f3bee1f2d6b50e4e545c5faf00fd50 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 22 Jan 2025 14:35:07 -0700 Subject: [PATCH 04/68] Update api report and size limits --- .api-reports/api-report-react.api.md | 1 + .api-reports/api-report-react_components.api.md | 1 + .api-reports/api-report-react_hoc.api.md | 1 + .api-reports/api-report-react_hooks.api.md | 1 + .api-reports/api-report.api.md | 1 + .size-limits.json | 2 +- 6 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index bd6bae479c4..5b039e88eb5 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -379,6 +379,7 @@ type BackgroundQueryHookOptionsNoInfer = ApolloCache> extends MutationSharedOptions { client?: ApolloClient; + // @deprecated ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index b1c44362975..5a3d468336c 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -327,6 +327,7 @@ type AsStoreObject = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts client?: ApolloClient; + // @deprecated ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index 25ca4e27760..576be52aa8c 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -326,6 +326,7 @@ type AsStoreObject = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts client?: ApolloClient; + // @deprecated ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index b293da7529f..4ba7a5a695c 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -350,6 +350,7 @@ type BackgroundQueryHookOptionsNoInfer = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts client?: ApolloClient; + // @deprecated ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 5d15453120a..2cfcdacce58 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -348,6 +348,7 @@ type BackgroundQueryHookOptionsNoInfer = ApolloCache> extends MutationSharedOptions { client?: ApolloClient; + // @deprecated ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; diff --git a/.size-limits.json b/.size-limits.json index f7f8f2606dc..605be6f7f07 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41642, + "dist/apollo-client.min.cjs": 41640, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34384 } From ec7e46c6c627daf882a4746a484d58837c258da3 Mon Sep 17 00:00:00 2001 From: "Michael \"Mike\" Ferris" Date: Mon, 27 Jan 2025 04:07:49 -0500 Subject: [PATCH 05/68] Add more information about deprecation of useMutation ignoreResults option (#12301) * Add more information about deprecation of useMutation ignoreResults option * PR Review * Update src/react/types/types.ts --------- Co-authored-by: Lenz Weber-Tronic Co-authored-by: Lenz Weber-Tronic --- .changeset/khaki-cars-develop.md | 2 ++ src/react/types/types.ts | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.changeset/khaki-cars-develop.md b/.changeset/khaki-cars-develop.md index 7f02084721f..09022ce6b89 100644 --- a/.changeset/khaki-cars-develop.md +++ b/.changeset/khaki-cars-develop.md @@ -3,3 +3,5 @@ --- Deprecate option `ignoreResults` in `useMutation`. +Once this option is removed, existing code still using it might see increase in re-renders. +If you don't want to synchronize your component state with the mutation, please use `useApolloClient` to get your ApolloClient instance and call `client.mutate` directly. diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 3f8160bdfb5..f3ce870c714 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -360,7 +360,9 @@ export interface BaseMutationOptions< onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; /** * {@inheritDoc @apollo/client!MutationOptionsDocumentation#ignoreResults:member} - * @deprecated This property will be removed in the next major version of Apollo Client + * + * @deprecated This option will be removed in the next major version of Apollo Client. + * If you don't want to synchronize your component state with the mutation, please use `useApolloClient` to get your ApolloClient instance and call `client.mutate` directly. */ ignoreResults?: boolean; } From b17968b61f0e35b1ba20d081dacee66af8225491 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 3 Feb 2025 10:50:24 -0700 Subject: [PATCH 06/68] Allow protocol errors to be retryable in `RetryLink` (#12318) --- .changeset/thin-oranges-laugh.md | 18 ++++++++ src/link/retry/__tests__/retryLink.ts | 66 ++++++++++++++++++++++++++- src/link/retry/retryLink.ts | 21 ++++++++- 3 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 .changeset/thin-oranges-laugh.md diff --git a/.changeset/thin-oranges-laugh.md b/.changeset/thin-oranges-laugh.md new file mode 100644 index 00000000000..9cda6f16002 --- /dev/null +++ b/.changeset/thin-oranges-laugh.md @@ -0,0 +1,18 @@ +--- +"@apollo/client": patch +--- + +Allow `RetryLink` to retry an operation when fatal [transport-level errors](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol#message-and-error-format) are emitted from multipart subscriptions. + +```js +const retryLink = new RetryLink({ + attempts: (count, operation, error) => { + if (error instanceof ApolloError) { + // errors available on the `protocolErrors` field in `ApolloError` + console.log(error.protocolErrors) + } + + return true; + } +}); +``` diff --git a/src/link/retry/__tests__/retryLink.ts b/src/link/retry/__tests__/retryLink.ts index 85955021588..973b8ab92e6 100644 --- a/src/link/retry/__tests__/retryLink.ts +++ b/src/link/retry/__tests__/retryLink.ts @@ -5,7 +5,11 @@ import { execute } from "../../core/execute"; import { Observable } from "../../../utilities/observables/Observable"; import { fromError } from "../../utils/fromError"; import { RetryLink } from "../retryLink"; -import { ObservableStream } from "../../../testing/internal"; +import { + mockMultipartSubscriptionStream, + ObservableStream, +} from "../../../testing/internal"; +import { ApolloError } from "../../../core"; const query = gql` { @@ -210,4 +214,64 @@ describe("RetryLink", () => { [3, operation, standardError], ]); }); + + it("handles protocol errors from multipart subscriptions", async () => { + const subscription = gql` + subscription MySubscription { + aNewDieWasCreated { + die { + roll + sides + color + } + } + } + `; + + const attemptStub = jest.fn(); + attemptStub.mockReturnValueOnce(true); + + const retryLink = new RetryLink({ + delay: { initial: 1 }, + attempts: attemptStub, + }); + + const { httpLink, enqueuePayloadResult, enqueueProtocolErrors } = + mockMultipartSubscriptionStream(); + const link = ApolloLink.from([retryLink, httpLink]); + const stream = new ObservableStream(execute(link, { query: subscription })); + + enqueueProtocolErrors([ + { message: "Error field", extensions: { code: "INTERNAL_SERVER_ERROR" } }, + ]); + + enqueuePayloadResult({ + data: { + aNewDieWasCreated: { die: { color: "blue", roll: 2, sides: 6 } }, + }, + }); + + await expect(stream).toEmitValue({ + data: { + aNewDieWasCreated: { die: { color: "blue", roll: 2, sides: 6 } }, + }, + }); + + expect(attemptStub).toHaveBeenCalledTimes(1); + expect(attemptStub).toHaveBeenCalledWith( + 1, + expect.objectContaining({ + operationName: "MySubscription", + query: subscription, + }), + new ApolloError({ + protocolErrors: [ + { + message: "Error field", + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }, + ], + }) + ); + }); }); diff --git a/src/link/retry/retryLink.ts b/src/link/retry/retryLink.ts index cde2dd2ea9c..37c293d99df 100644 --- a/src/link/retry/retryLink.ts +++ b/src/link/retry/retryLink.ts @@ -7,6 +7,11 @@ import { buildDelayFunction } from "./delayFunction.js"; import type { RetryFunction, RetryFunctionOptions } from "./retryFunction.js"; import { buildRetryFunction } from "./retryFunction.js"; import type { SubscriptionObserver } from "zen-observable-ts"; +import { + ApolloError, + graphQLResultHasProtocolErrors, + PROTOCOL_ERRORS_SYMBOL, +} from "../../errors/index.js"; export namespace RetryLink { export interface Options { @@ -54,7 +59,21 @@ class RetryableOperation { private try() { this.currentSubscription = this.forward(this.operation).subscribe({ - next: this.observer.next.bind(this.observer), + next: (result) => { + if (graphQLResultHasProtocolErrors(result)) { + this.onError( + new ApolloError({ + protocolErrors: result.extensions[PROTOCOL_ERRORS_SYMBOL], + }) + ); + // Unsubscribe from the current subscription to prevent the `complete` + // handler to be called as a result of the stream closing. + this.currentSubscription?.unsubscribe(); + return; + } + + this.observer.next(result); + }, error: this.onError, complete: this.observer.complete.bind(this.observer), }); From 1641eeb04c3f857f40a890f6543126f020941a41 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 10:55:51 -0700 Subject: [PATCH 07/68] Version Packages (#12325) Co-authored-by: github-actions[bot] --- .changeset/curvy-seahorses-walk.md | 5 ----- .changeset/thin-oranges-laugh.md | 18 ------------------ CHANGELOG.md | 21 +++++++++++++++++++++ package-lock.json | 6 +++--- package.json | 2 +- 5 files changed, 25 insertions(+), 27 deletions(-) delete mode 100644 .changeset/curvy-seahorses-walk.md delete mode 100644 .changeset/thin-oranges-laugh.md diff --git a/.changeset/curvy-seahorses-walk.md b/.changeset/curvy-seahorses-walk.md deleted file mode 100644 index 377f9ee5803..00000000000 --- a/.changeset/curvy-seahorses-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fix type of `extensions` in `protocolErrors` for `ApolloError` and the `onError` link. According to the [multipart HTTP subscription protocol](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol), fatal tranport errors follow the [GraphQL error format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) which require `extensions` to be a map as its value instead of an array. diff --git a/.changeset/thin-oranges-laugh.md b/.changeset/thin-oranges-laugh.md deleted file mode 100644 index 9cda6f16002..00000000000 --- a/.changeset/thin-oranges-laugh.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -"@apollo/client": patch ---- - -Allow `RetryLink` to retry an operation when fatal [transport-level errors](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol#message-and-error-format) are emitted from multipart subscriptions. - -```js -const retryLink = new RetryLink({ - attempts: (count, operation, error) => { - if (error instanceof ApolloError) { - // errors available on the `protocolErrors` field in `ApolloError` - console.log(error.protocolErrors) - } - - return true; - } -}); -``` diff --git a/CHANGELOG.md b/CHANGELOG.md index fb62dad7b77..de09aed465e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # @apollo/client +## 3.12.9 + +### Patch Changes + +- [#12321](https://github.com/apollographql/apollo-client/pull/12321) [`daa4f33`](https://github.com/apollographql/apollo-client/commit/daa4f3303cfb81e8dca66c21ce3f3dc24946cafb) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix type of `extensions` in `protocolErrors` for `ApolloError` and the `onError` link. According to the [multipart HTTP subscription protocol](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol), fatal tranport errors follow the [GraphQL error format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) which require `extensions` to be a map as its value instead of an array. + +- [#12318](https://github.com/apollographql/apollo-client/pull/12318) [`b17968b`](https://github.com/apollographql/apollo-client/commit/b17968b61f0e35b1ba20d081dacee66af8225491) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Allow `RetryLink` to retry an operation when fatal [transport-level errors](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol#message-and-error-format) are emitted from multipart subscriptions. + + ```js + const retryLink = new RetryLink({ + attempts: (count, operation, error) => { + if (error instanceof ApolloError) { + // errors available on the `protocolErrors` field in `ApolloError` + console.log(error.protocolErrors); + } + + return true; + }, + }); + ``` + ## 3.12.8 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 4949c78795e..b8452c3f72e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.12.8", + "version": "3.12.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.12.8", + "version": "3.12.9", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -25,7 +25,7 @@ "zen-observable-ts": "^1.2.5" }, "devDependencies": { - "@actions/github-script": "github:actions/github-script#v7", + "@actions/github-script": "github:actions/github-script#v7.0.1", "@arethetypeswrong/cli": "0.15.3", "@ark/attest": "0.28.0", "@babel/parser": "7.25.0", diff --git a/package.json b/package.json index b5ad466cc0e..de921768aa7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.12.8", + "version": "3.12.9", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 92db870d5ee8307ba85dd743a70f77b7e5154183 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 17:57:14 +0000 Subject: [PATCH 08/68] Prepare for rc release --- .changeset/pre.json | 4 ++-- package-lock.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 461875c7499..ba55303eb3c 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,8 +1,8 @@ { "mode": "pre", - "tag": "alpha", + "tag": "rc", "initialVersions": { "@apollo/client": "3.12.2" }, "changesets": [] -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4949c78795e..47ad09c384c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,7 +25,7 @@ "zen-observable-ts": "^1.2.5" }, "devDependencies": { - "@actions/github-script": "github:actions/github-script#v7", + "@actions/github-script": "github:actions/github-script#v7.0.1", "@arethetypeswrong/cli": "0.15.3", "@ark/attest": "0.28.0", "@babel/parser": "7.25.0", From e63a32681026c5f27abead0717787b0c5e304fdc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 4 Feb 2025 10:08:50 -0700 Subject: [PATCH 09/68] chore(deps): update cimg/node docker tag to v23.7.0 (#12336) --- .circleci/config.yml | 14 +++++++------- .size-limits.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index c8751534afe..0b3aee29c12 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -15,7 +15,7 @@ jobs: Lint: docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.7.0 steps: - checkout - run: npm version @@ -24,7 +24,7 @@ jobs: Formatting: docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.7.0 steps: - checkout - run: npm ci @@ -32,7 +32,7 @@ jobs: Tests: docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.7.0 parameters: project: type: string @@ -53,7 +53,7 @@ jobs: path: reports/junit Attest: docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.7.0 steps: - checkout - run: npm ci @@ -61,7 +61,7 @@ jobs: BuildTarball: docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.7.0 steps: - checkout - run: npm run ci:precheck @@ -80,7 +80,7 @@ jobs: react: type: string docker: - - image: cimg/node:23.6.0-browsers + - image: cimg/node:23.7.0-browsers steps: - checkout - attach_workspace: @@ -118,7 +118,7 @@ jobs: externalPackage: type: string docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.7.0 steps: - checkout - attach_workspace: diff --git a/.size-limits.json b/.size-limits.json index f7f8f2606dc..a808e1e2da7 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41642, + "dist/apollo-client.min.cjs": 41643, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34384 } From c01da5da639d4d9e882d380573b7876df4a1d65b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 4 Feb 2025 16:21:19 -0700 Subject: [PATCH 10/68] Feature: `useSuspenseFragment` (#12066) --- .api-reports/api-report-react.api.md | 42 +- .api-reports/api-report-react_hooks.api.md | 42 +- .api-reports/api-report-react_internal.api.md | 109 +- .api-reports/api-report.api.md | 38 + .changeset/blue-comics-train.md | 7 + .size-limits.json | 2 +- config/jest.config.js | 1 + src/__tests__/__snapshots__/exports.ts.snap | 3 + .../__tests__/useSuspenseFragment.test.tsx | 1850 +++++++++++++++++ src/react/hooks/index.ts | 2 + src/react/hooks/internal/wrapHook.ts | 2 + src/react/hooks/useSuspenseFragment.ts | 167 ++ src/react/internal/cache/FragmentReference.ts | 200 ++ src/react/internal/cache/SuspenseCache.ts | 34 +- src/react/internal/cache/types.ts | 10 + 15 files changed, 2498 insertions(+), 11 deletions(-) create mode 100644 .changeset/blue-comics-train.md create mode 100644 src/react/hooks/__tests__/useSuspenseFragment.test.tsx create mode 100644 src/react/hooks/useSuspenseFragment.ts create mode 100644 src/react/internal/cache/FragmentReference.ts diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 76e0177b033..f2da5216c6e 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -886,6 +886,11 @@ TData }; } : never : never; +// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type From = StoreObject | Reference | FragmentType> | string | null; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -2317,8 +2322,6 @@ export function useFragment(options: Us // @public (undocumented) export interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; - // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts - // // (undocumented) from: StoreObject | Reference | FragmentType> | string | null; // (undocumented) @@ -2413,6 +2416,40 @@ export function useSubscription(options: UseSuspenseFragmentOptions & { + from: NonNullable>; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: null; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: From; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; + +// @public (undocumented) +interface UseSuspenseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { + client?: ApolloClient; + // (undocumented) + from: From; + // (undocumented) + optimistic?: boolean; +} + +// @public (undocumented) +export type UseSuspenseFragmentResult = { + data: MaybeMasked; +}; + // @public (undocumented) export function useSuspenseQuery, "variables">>(query: DocumentNode | TypedDocumentNode, options?: SuspenseQueryHookOptions, NoInfer_2> & TOptions): UseSuspenseQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? TOptions["skip"] extends boolean ? DeepPartial | undefined : DeepPartial : TOptions["skip"] extends boolean ? TData | undefined : TData, TVariables>; @@ -2531,6 +2568,7 @@ interface WatchQueryOptions = StoreObject | Reference | FragmentType> | string | null; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -2141,8 +2146,6 @@ export function useFragment(options: Us // @public (undocumented) export interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; - // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts - // // (undocumented) from: StoreObject | Reference | FragmentType> | string | null; // (undocumented) @@ -2246,6 +2249,40 @@ export function useSubscription(options: UseSuspenseFragmentOptions & { + from: NonNullable>; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: null; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: From; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; + +// @public (undocumented) +interface UseSuspenseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { + client?: ApolloClient; + // (undocumented) + from: From; + // (undocumented) + optimistic?: boolean; +} + +// @public (undocumented) +export type UseSuspenseFragmentResult = { + data: MaybeMasked; +}; + // Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -2355,6 +2392,7 @@ interface WatchQueryOptions; } +// @public (undocumented) +type FragmentCacheKey = [ +cacheId: string, +fragment: DocumentNode, +stringifiedVariables: string +]; + +// @public (undocumented) +interface FragmentKey { + // (undocumented) + __fragmentKey?: string; +} + // @public interface FragmentMap { // (undocumented) @@ -816,6 +829,43 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +class FragmentReference> { + // Warning: (ae-forgotten-export) The symbol "FragmentReferenceOptions" needs to be exported by the entry point index.d.ts + constructor(client: ApolloClient, watchFragmentOptions: WatchFragmentOptions & { + from: string; + }, options: FragmentReferenceOptions); + // Warning: (ae-forgotten-export) The symbol "FragmentKey" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly key: FragmentKey; + // Warning: (ae-forgotten-export) The symbol "Listener_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + listen(listener: Listener_2>): () => void; + // (undocumented) + readonly observable: Observable>; + // Warning: (ae-forgotten-export) The symbol "FragmentRefPromise" needs to be exported by the entry point index.d.ts + // + // (undocumented) + promise: FragmentRefPromise>; + // (undocumented) + retain(): () => void; +} + +// @public (undocumented) +interface FragmentReferenceOptions { + // (undocumented) + autoDisposeTimeoutMs?: number; + // (undocumented) + onDispose?: () => void; +} + +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type FragmentRefPromise = PromiseWithState; + // @public (undocumented) type FragmentType = [ TData @@ -827,6 +877,11 @@ TData }; } : never : never; +// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type From = StoreObject | Reference | FragmentType> | string | null; + // @public (undocumented) interface FulfilledPromise extends Promise { // (undocumented) @@ -1036,6 +1091,9 @@ type IsStrictlyAny = UnionToIntersection> extends never ? true // @public (undocumented) type Listener = (promise: QueryRefPromise) => void; +// @public (undocumented) +type Listener_2 = (promise: FragmentRefPromise) => void; + // @public (undocumented) class LocalState { // Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts @@ -1766,8 +1824,6 @@ export interface QueryReference extends Q toPromise?: unknown; } -// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts -// // @public (undocumented) type QueryRefPromise = PromiseWithState>>; @@ -1999,6 +2055,13 @@ class SuspenseCache { constructor(options?: SuspenseCacheOptions); // (undocumented) add(cacheKey: CacheKey, queryRef: InternalQueryReference): void; + // Warning: (ae-forgotten-export) The symbol "FragmentCacheKey" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "FragmentReference" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getFragmentRef(cacheKey: FragmentCacheKey, client: ApolloClient, options: WatchFragmentOptions & { + from: string; + }): FragmentReference; // (undocumented) getQueryRef(cacheKey: CacheKey, createObservable: () => ObservableQuery): InternalQueryReference; } @@ -2203,8 +2266,6 @@ function useFragment(options: UseFragme // @public (undocumented) interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; - // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts - // // (undocumented) from: StoreObject | Reference | FragmentType> | string | null; // (undocumented) @@ -2251,6 +2312,41 @@ interface UseReadQueryResult { networkStatus: NetworkStatus; } +// Warning: (ae-forgotten-export) The symbol "UseSuspenseFragmentOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UseSuspenseFragmentResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: NonNullable>; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: null; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: From; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; + +// @public (undocumented) +interface UseSuspenseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { + client?: ApolloClient; + // (undocumented) + from: From; + // (undocumented) + optimistic?: boolean; +} + +// @public (undocumented) +type UseSuspenseFragmentResult = { + data: MaybeMasked; +}; + // Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UseSuspenseQueryResult" needs to be exported by the entry point index.d.ts // @@ -2378,6 +2474,10 @@ interface WrappableHooks { // // (undocumented) useReadQuery: typeof useReadQuery; + // Warning: (ae-forgotten-export) The symbol "useSuspenseFragment" needs to be exported by the entry point index.d.ts + // + // (undocumented) + useSuspenseFragment: typeof useSuspenseFragment; // Warning: (ae-forgotten-export) The symbol "useSuspenseQuery" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2416,6 +2516,7 @@ export function wrapQueryRef(inter // src/react/hooks/useBackgroundQuery.ts:38:3 - (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:54:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:78:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useSuspenseFragment.ts:60:5 - (ae-forgotten-export) The symbol "From" needs to be exported by the entry point index.d.ts // src/react/query-preloader/createQueryPreloader.ts:145:3 - (ae-forgotten-export) The symbol "PreloadQueryFetchPolicy" needs to be exported by the entry point index.d.ts // src/react/query-preloader/createQueryPreloader.ts:167:5 - (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index f4828ef5b43..cfb98c0fbf3 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -1094,6 +1094,9 @@ TData }; } : never : never; +// @public (undocumented) +type From = StoreObject | Reference | FragmentType> | string | null; + // @public (undocumented) export const from: typeof ApolloLink.from; @@ -3084,6 +3087,40 @@ export function useSubscription(options: UseSuspenseFragmentOptions & { + from: NonNullable>; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: null; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: From; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; + +// @public (undocumented) +interface UseSuspenseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { + client?: ApolloClient; + // (undocumented) + from: From; + // (undocumented) + optimistic?: boolean; +} + +// @public (undocumented) +export type UseSuspenseFragmentResult = { + data: MaybeMasked; +}; + // @public (undocumented) export function useSuspenseQuery, "variables">>(query: DocumentNode | TypedDocumentNode, options?: SuspenseQueryHookOptions, NoInfer_2> & TOptions): UseSuspenseQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? TOptions["skip"] extends boolean ? DeepPartial | undefined : DeepPartial : TOptions["skip"] extends boolean ? TData | undefined : TData, TVariables>; @@ -3230,6 +3267,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/react/hooks/useBackgroundQuery.ts:54:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:78:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useLoadableQuery.ts:120:9 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useSuspenseFragment.ts:60:5 - (ae-forgotten-export) The symbol "From" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.changeset/blue-comics-train.md b/.changeset/blue-comics-train.md new file mode 100644 index 00000000000..533df0e9022 --- /dev/null +++ b/.changeset/blue-comics-train.md @@ -0,0 +1,7 @@ +--- +"@apollo/client": minor +--- + +Adds a new `useSuspenseFragment` hook. + +`useSuspenseFragment` suspends until `data` is complete. It is a drop-in replacement for `useFragment` when you prefer to use Suspense to control the loading state of a fragment. diff --git a/.size-limits.json b/.size-limits.json index 605be6f7f07..4d4b64c5f17 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41640, + "dist/apollo-client.min.cjs": 42176, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34384 } diff --git a/config/jest.config.js b/config/jest.config.js index 977c2e8e80a..011836cc41f 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -44,6 +44,7 @@ const react17TestFileIgnoreList = [ // We only support Suspense with React 18, so don't test suspense hooks with // React 17 "src/testing/experimental/__tests__/createTestSchema.test.tsx", + "src/react/hooks/__tests__/useSuspenseFragment.test.tsx", "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", "src/react/hooks/__tests__/useLoadableQuery.test.tsx", diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index e21e634c864..379695ffb14 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -67,6 +67,7 @@ Array [ "useReactiveVar", "useReadQuery", "useSubscription", + "useSuspenseFragment", "useSuspenseQuery", ] `; @@ -293,6 +294,7 @@ Array [ "useReactiveVar", "useReadQuery", "useSubscription", + "useSuspenseFragment", "useSuspenseQuery", ] `; @@ -338,6 +340,7 @@ Array [ "useReactiveVar", "useReadQuery", "useSubscription", + "useSuspenseFragment", "useSuspenseQuery", ] `; diff --git a/src/react/hooks/__tests__/useSuspenseFragment.test.tsx b/src/react/hooks/__tests__/useSuspenseFragment.test.tsx new file mode 100644 index 00000000000..108333917b9 --- /dev/null +++ b/src/react/hooks/__tests__/useSuspenseFragment.test.tsx @@ -0,0 +1,1850 @@ +import { + useSuspenseFragment, + UseSuspenseFragmentResult, +} from "../useSuspenseFragment"; +import { + ApolloClient, + FragmentType, + gql, + InMemoryCache, + Masked, + MaskedDocumentNode, + MaybeMasked, + TypedDocumentNode, +} from "../../../core"; +import React, { Suspense } from "react"; +import { ApolloProvider } from "../../context"; +import { + createRenderStream, + disableActEnvironment, + renderHookToSnapshotStream, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import { renderAsync, spyOnConsole } from "../../../testing/internal"; +import { act, renderHook, screen, waitFor } from "@testing-library/react"; +import { InvariantError } from "ts-invariant"; +import { MockedProvider, MockSubscriptionLink, wait } from "../../../testing"; +import { expectTypeOf } from "expect-type"; +import userEvent from "@testing-library/user-event"; + +function createDefaultRenderStream() { + return createRenderStream({ + initialSnapshot: { + result: null as UseSuspenseFragmentResult> | null, + }, + }); +} + +function createDefaultTrackedComponents() { + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + return { SuspenseFallback }; +} + +test("validates the GraphQL document is a fragment", () => { + using _ = spyOnConsole("error"); + + const fragment = gql` + query ShouldThrow { + createException + } + `; + + expect(() => { + renderHook( + () => useSuspenseFragment({ fragment, from: { __typename: "Nope" } }), + { wrapper: ({ children }) => {children} } + ); + }).toThrow( + new InvariantError( + "Found a query operation named 'ShouldThrow'. No operations are allowed when using a fragment as a query. Only fragments are allowed." + ) + ); +}); + +test("throws if no client is provided", () => { + using _spy = spyOnConsole("error"); + expect(() => + renderHook(() => + useSuspenseFragment({ + fragment: gql` + fragment ShouldThrow on Error { + shouldThrow + } + `, + from: {}, + }) + ) + ).toThrow(/pass an ApolloClient/); +}); + +test("suspends until cache value is complete", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const { render, takeRender, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + function App() { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => { + return {children}; + }, + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("updates when the cache updates", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const { takeRender, render, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + function App() { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("resuspends when data goes missing until complete again", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const { takeRender, render, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + function App() { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + client.cache.modify({ + id: "Item:1", + fields: { + text: (_, { DELETE }) => DELETE, + }, + }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("does not suspend and returns cache data when data is already in the cache", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const { takeRender, render, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Cached" }, + }); + + function App() { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Cached", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("receives cache updates after initial result when data is written to the cache before mounted", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const { takeRender, render, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Cached" }, + }); + + function App() { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Cached", + }, + }); + } + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Updated" }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Updated", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("allows the client to be overridden", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const defaultClient = new ApolloClient({ cache: new InMemoryCache() }); + const client = new ApolloClient({ cache: new InMemoryCache() }); + + defaultClient.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Should not be used" }, + }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ + fragment, + client, + from: { __typename: "Item", id: 1 }, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + const { data } = await takeSnapshot(); + + expect(data).toEqual({ __typename: "Item", id: 1, text: "Item #1" }); +}); + +test("suspends until data is complete when changing `from` with no data written to cache", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const { takeRender, replaceSnapshot, render } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + using _disabledAct = disableActEnvironment(); + function App({ id }: { id: number }) { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id }, + }); + + replaceSnapshot({ result }); + + return null; + } + + const { rerender } = await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + await rerender( + }> + + + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 2, text: "Item #2" }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 2, + text: "Item #2", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("does not suspend when changing `from` with data already written to cache", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const { takeRender, replaceSnapshot, render } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 2, text: "Item #2" }, + }); + + using _disabledAct = disableActEnvironment(); + function App({ id }: { id: number }) { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id }, + }); + + replaceSnapshot({ result }); + + return null; + } + + const { rerender } = await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + await rerender( + }> + + + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 2, + text: "Item #2", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +it("does not rerender when fields with @nonreactive change", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text @nonreactive + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + using _disabledAct = disableActEnvironment(); + + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ fragment, from: { __typename: "Item", id: 1 } }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ + __typename: "Item", + id: 1, + text: "Item #1", + }); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +it("does not rerender when fields with @nonreactive on nested fragment change", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + ...ItemFields @nonreactive + } + + fragment ItemFields on Item { + text + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + fragmentName: "ItemFragment", + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + using _disabledAct = disableActEnvironment(); + + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ + fragment, + fragmentName: "ItemFragment", + from: { __typename: "Item", id: 1 }, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ + __typename: "Item", + id: 1, + text: "Item #1", + }); + } + + client.writeFragment({ + fragment, + fragmentName: "ItemFragment", + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +// TODO: Update when https://github.com/apollographql/apollo-client/issues/12003 is fixed +it.failing( + "warns and suspends when passing parent object to `from` when key fields are missing", + async () => { + using _ = spyOnConsole("warn"); + + interface Fragment { + age: number; + } + + const fragment: TypedDocumentNode = gql` + fragment UserFields on User { + age + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const { replaceSnapshot, render, takeRender } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + function App() { + const result = useSuspenseFragment({ + fragment, + from: { __typename: "User" }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Could not identify object passed to `from` for '%s' fragment, either because the object is non-normalized or the key fields are missing. If you are masking this object, please ensure the key fields are requested by the parent object.", + "UserFields" + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + } +); + +test("returns null if `from` is `null`", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useSuspenseFragment({ fragment, from: null }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + const { data } = await takeSnapshot(); + + expect(data).toBeNull(); +}); + +test("returns cached value when `from` changes from `null` to non-null value", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + ({ id }) => + useSuspenseFragment({ + fragment, + from: id === null ? null : { __typename: "Item", id }, + }), + { + initialProps: { id: null as null | number }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toBeNull(); + } + + await rerender({ id: 1 }); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ + __typename: "Item", + id: 1, + text: "Item #1", + }); + } + + await expect(takeSnapshot).not.toRerender(); +}); + +test("returns null value when `from` changes from non-null value to `null`", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + ({ id }) => + useSuspenseFragment({ + fragment, + from: id === null ? null : { __typename: "Item", id }, + }), + { + initialProps: { id: 1 as null | number }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ + __typename: "Item", + id: 1, + text: "Item #1", + }); + } + + await rerender({ id: null }); + + { + const { data } = await takeSnapshot(); + + expect(data).toBeNull(); + } + + await expect(takeSnapshot).not.toRerender(); +}); + +test("suspends until cached value is available when `from` changes from `null` to non-null value", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const { takeRender, render, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + function App({ id }: { id: number | null }) { + useTrackRenders(); + const result = useSuspenseFragment({ + fragment, + from: id === null ? null : { __typename: "Item", id }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + const { rerender } = await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ data: null }); + } + + await rerender( + }> + + + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("returns masked fragment when data masking is enabled", async () => { + type Post = { + __typename: "Post"; + id: number; + title: string; + } & { " $fragmentRefs"?: { PostFields: PostFields } }; + + type PostFields = { + __typename: "Post"; + updatedAt: string; + } & { " $fragmentName"?: "PostFields" }; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + const fragment: TypedDocumentNode = gql` + fragment PostFragment on Post { + id + title + ...PostFields + } + + fragment PostFields on Post { + updatedAt + } + `; + + client.writeFragment({ + fragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + updatedAt: "2024-01-01", + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ + fragment, + fragmentName: "PostFragment", + from: { __typename: "Post", id: 1 }, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const snapshot = await takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }); + } + + await expect(takeSnapshot).not.toRerender(); +}); + +test("does not rerender for cache writes to masked fields", async () => { + type Post = { + __typename: "Post"; + id: number; + title: string; + } & { " $fragmentRefs"?: { PostFields: PostFields } }; + + type PostFields = { + __typename: "Post"; + updatedAt: string; + } & { " $fragmentName"?: "PostFields" }; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + const fragment: TypedDocumentNode = gql` + fragment PostFragment on Post { + id + title + ...PostFields + } + + fragment PostFields on Post { + updatedAt + } + `; + + client.writeFragment({ + fragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + updatedAt: "2024-01-01", + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ + fragment, + fragmentName: "PostFragment", + from: { __typename: "Post", id: 1 }, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const snapshot = await takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }); + } + + client.writeFragment({ + fragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + updatedAt: "2024-02-01", + }, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test("updates child fragments for cache updates to masked fields", async () => { + type Post = { + __typename: "Post"; + id: number; + title: string; + } & { " $fragmentRefs"?: { PostFields: PostFields } }; + + type PostFields = { + __typename: "Post"; + updatedAt: string; + } & { " $fragmentName"?: "PostFields" }; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + const postFieldsFragment: MaskedDocumentNode = gql` + fragment PostFields on Post { + updatedAt + } + `; + + const postFragment: MaskedDocumentNode = gql` + fragment PostFragment on Post { + id + title + ...PostFields + } + + ${postFieldsFragment} + `; + + client.writeFragment({ + fragment: postFragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + updatedAt: "2024-01-01", + }, + }); + + const { render, mergeSnapshot, takeRender } = createRenderStream({ + initialSnapshot: { + parent: null as UseSuspenseFragmentResult> | null, + child: null as UseSuspenseFragmentResult> | null, + }, + }); + + function Parent() { + useTrackRenders(); + const parent = useSuspenseFragment({ + fragment: postFragment, + fragmentName: "PostFragment", + from: { __typename: "Post", id: 1 }, + }); + + mergeSnapshot({ parent }); + + return ; + } + + function Child({ post }: { post: FragmentType }) { + useTrackRenders(); + const child = useSuspenseFragment({ + fragment: postFieldsFragment, + from: post, + }); + + mergeSnapshot({ child }); + return null; + } + + using _disabledAct = disableActEnvironment(); + await render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { snapshot } = await takeRender(); + + expect(snapshot).toEqual({ + parent: { + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }, + child: { + data: { + __typename: "Post", + updatedAt: "2024-01-01", + }, + }, + }); + } + + client.writeFragment({ + fragment: postFragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + updatedAt: "2024-02-01", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([Child]); + expect(snapshot).toEqual({ + parent: { + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }, + child: { + data: { + __typename: "Post", + updatedAt: "2024-02-01", + }, + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("tears down the subscription on unmount", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ cache }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + using _disabledAct = disableActEnvironment(); + const { unmount, takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ fragment, from: { __typename: "Item", id: 1 } }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ __typename: "Item", id: 1, text: "Item #1" }); + } + + expect(cache["watches"].size).toBe(1); + + unmount(); + // We need to wait a tick since the cleanup is run in a setTimeout to + // prevent strict mode bugs. + await wait(0); + + expect(cache["watches"].size).toBe(0); +}); + +test("tears down all watches when rendering multiple records", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ cache }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 2, text: "Item #2" }, + }); + + using _disabledAct = disableActEnvironment(); + const { unmount, rerender, takeSnapshot } = await renderHookToSnapshotStream( + ({ id }) => + useSuspenseFragment({ fragment, from: { __typename: "Item", id } }), + { + initialProps: { id: 1 }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ __typename: "Item", id: 1, text: "Item #1" }); + } + + await rerender({ id: 2 }); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ __typename: "Item", id: 2, text: "Item #2" }); + } + + unmount(); + // We need to wait a tick since the cleanup is run in a setTimeout to + // prevent strict mode bugs. + await wait(0); + + expect(cache["watches"].size).toBe(0); +}); + +test("tears down watches after default autoDisposeTimeoutMs if component never renders again after suspending", async () => { + jest.useFakeTimers(); + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const cache = new InMemoryCache(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ link, cache }); + + function App() { + const [showItem, setShowItem] = React.useState(true); + + return ( + + + {showItem && ( + + + + )} + + ); + } + + function Item() { + const { data } = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + return {data.text}; + } + + await renderAsync(); + + // Ensure suspends immediately + expect(screen.getByText("Loading item...")).toBeInTheDocument(); + + // Hide the greeting before it finishes loading data + await act(() => user.click(screen.getByText("Hide item"))); + + expect(screen.queryByText("Loading item...")).not.toBeInTheDocument(); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + // clear the microtask queue + await act(() => Promise.resolve()); + + expect(cache["watches"].size).toBe(1); + + jest.advanceTimersByTime(30_000); + + expect(cache["watches"].size).toBe(0); + + jest.useRealTimers(); +}); + +test("tears down watches after configured autoDisposeTimeoutMs if component never renders again after suspending", async () => { + jest.useFakeTimers(); + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + link, + cache, + defaultOptions: { + react: { + suspense: { + autoDisposeTimeoutMs: 5000, + }, + }, + }, + }); + + function App() { + const [showItem, setShowItem] = React.useState(true); + + return ( + + + {showItem && ( + + + + )} + + ); + } + + function Item() { + const { data } = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + return {data.text}; + } + + await renderAsync(); + + // Ensure suspends immediately + expect(screen.getByText("Loading item...")).toBeInTheDocument(); + + // Hide the greeting before it finishes loading data + await act(() => user.click(screen.getByText("Hide item"))); + + expect(screen.queryByText("Loading item...")).not.toBeInTheDocument(); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + // clear the microtask queue + await act(() => Promise.resolve()); + + expect(cache["watches"].size).toBe(1); + + jest.advanceTimersByTime(5000); + + expect(cache["watches"].size).toBe(0); + + jest.useRealTimers(); +}); + +test("cancels autoDisposeTimeoutMs if the component renders before timer finishes", async () => { + jest.useFakeTimers(); + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + const client = new ApolloClient({ link, cache }); + + function App() { + return ( + + + + + + ); + } + + function Item() { + const { data } = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + return {data.text}; + } + + await renderAsync(); + + // Ensure suspends immediately + expect(screen.getByText("Loading item...")).toBeInTheDocument(); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + // clear the microtask queue + await act(() => Promise.resolve()); + + await waitFor(() => { + expect(screen.getByText("Item #1")).toBeInTheDocument(); + }); + + jest.advanceTimersByTime(30_000); + + expect(cache["watches"].size).toBe(1); + + jest.useRealTimers(); +}); + +describe.skip("type tests", () => { + test("returns TData when from is a non-null value", () => { + type Data = { foo: string }; + const fragment: TypedDocumentNode = gql``; + + { + const { data } = useSuspenseFragment({ + fragment, + from: { __typename: "Query" }, + }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + + { + const { data } = useSuspenseFragment({ + fragment: gql``, + from: { __typename: "Query" }, + }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + }); + + test("returns null when from is null", () => { + type Data = { foo: string }; + type Vars = Record; + const fragment: TypedDocumentNode = gql``; + + { + const { data } = useSuspenseFragment({ fragment, from: null }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + + { + const { data } = useSuspenseFragment({ + fragment: gql``, + from: null, + }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + }); + + test("returns TData | null when from is nullable", () => { + type Post = { __typename: "Post"; id: number }; + type Vars = Record; + const fragment: TypedDocumentNode = gql``; + const author = {} as { post: Post | null }; + + { + const { data } = useSuspenseFragment({ fragment, from: author.post }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + + { + const { data } = useSuspenseFragment({ + fragment: gql``, + from: author.post, + }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + }); +}); diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 78fc82c61f4..3fe6ccbc27d 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -11,6 +11,8 @@ export type { UseSuspenseQueryResult } from "./useSuspenseQuery.js"; export { useSuspenseQuery } from "./useSuspenseQuery.js"; export type { UseBackgroundQueryResult } from "./useBackgroundQuery.js"; export { useBackgroundQuery } from "./useBackgroundQuery.js"; +export type { UseSuspenseFragmentResult } from "./useSuspenseFragment.js"; +export { useSuspenseFragment } from "./useSuspenseFragment.js"; export type { LoadQueryFunction, UseLoadableQueryResult, diff --git a/src/react/hooks/internal/wrapHook.ts b/src/react/hooks/internal/wrapHook.ts index 59b112c3216..175d3e72a5b 100644 --- a/src/react/hooks/internal/wrapHook.ts +++ b/src/react/hooks/internal/wrapHook.ts @@ -5,6 +5,7 @@ import type { useReadQuery, useFragment, useQueryRefHandlers, + useSuspenseFragment, } from "../index.js"; import type { QueryManager } from "../../../core/QueryManager.js"; import type { ApolloClient } from "../../../core/ApolloClient.js"; @@ -17,6 +18,7 @@ interface WrappableHooks { createQueryPreloader: typeof createQueryPreloader; useQuery: typeof useQuery; useSuspenseQuery: typeof useSuspenseQuery; + useSuspenseFragment: typeof useSuspenseFragment; useBackgroundQuery: typeof useBackgroundQuery; useReadQuery: typeof useReadQuery; useFragment: typeof useFragment; diff --git a/src/react/hooks/useSuspenseFragment.ts b/src/react/hooks/useSuspenseFragment.ts new file mode 100644 index 00000000000..79aed6c8ff0 --- /dev/null +++ b/src/react/hooks/useSuspenseFragment.ts @@ -0,0 +1,167 @@ +import type { + ApolloClient, + OperationVariables, + Reference, + StoreObject, +} from "../../core/index.js"; +import { canonicalStringify } from "../../cache/index.js"; +import type { Cache } from "../../cache/index.js"; +import { useApolloClient } from "./useApolloClient.js"; +import { getSuspenseCache } from "../internal/index.js"; +import React, { useMemo } from "rehackt"; +import type { FragmentKey } from "../internal/cache/types.js"; +import { __use } from "./internal/__use.js"; +import { wrapHook } from "./internal/index.js"; +import type { FragmentType, MaybeMasked } from "../../masking/index.js"; +import type { NoInfer } from "../types/types.js"; + +type From = + | StoreObject + | Reference + | FragmentType> + | string + | null; + +export interface UseSuspenseFragmentOptions + extends Omit< + Cache.DiffOptions, NoInfer>, + "id" | "query" | "optimistic" | "previousResult" | "returnPartialData" + >, + Omit< + Cache.ReadFragmentOptions, + "id" | "variables" | "returnPartialData" + > { + from: From; + // Override this field to make it optional (default: true). + optimistic?: boolean; + /** + * The instance of `ApolloClient` to use to look up the fragment. + * + * 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?: ApolloClient; +} + +export type UseSuspenseFragmentResult = { data: MaybeMasked }; + +const NULL_PLACEHOLDER = [] as unknown as [ + FragmentKey, + Promise | null>, +]; + +export function useSuspenseFragment< + TData, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions & { + from: NonNullable>; + } +): UseSuspenseFragmentResult; + +export function useSuspenseFragment< + TData, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions & { + from: null; + } +): UseSuspenseFragmentResult; + +export function useSuspenseFragment< + TData, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions & { + from: From; + } +): UseSuspenseFragmentResult; + +export function useSuspenseFragment< + TData, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions +): UseSuspenseFragmentResult; + +export function useSuspenseFragment< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions +): UseSuspenseFragmentResult { + return wrapHook( + "useSuspenseFragment", + // eslint-disable-next-line react-compiler/react-compiler + useSuspenseFragment_, + useApolloClient(typeof options === "object" ? options.client : undefined) + )(options); +} + +function useSuspenseFragment_< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions +): UseSuspenseFragmentResult { + const client = useApolloClient(options.client); + const { from } = options; + const { cache } = client; + + const id = useMemo( + () => + typeof from === "string" ? from + : from === null ? null + : cache.identify(from), + [cache, from] + ) as string | null; + + const fragmentRef = + id === null ? null : ( + getSuspenseCache(client).getFragmentRef( + [id, options.fragment, canonicalStringify(options.variables)], + client, + { ...options, from: id } + ) + ); + + let [current, setPromise] = React.useState< + [FragmentKey, Promise | null>] + >( + fragmentRef === null ? NULL_PLACEHOLDER : ( + [fragmentRef.key, fragmentRef.promise] + ) + ); + + React.useEffect(() => { + if (fragmentRef === null) { + return; + } + + const dispose = fragmentRef.retain(); + const removeListener = fragmentRef.listen((promise) => { + setPromise([fragmentRef.key, promise]); + }); + + return () => { + dispose(); + removeListener(); + }; + }, [fragmentRef]); + + if (fragmentRef === null) { + return { data: null }; + } + + if (current[0] !== fragmentRef.key) { + // eslint-disable-next-line react-compiler/react-compiler + current[0] = fragmentRef.key; + current[1] = fragmentRef.promise; + } + + const data = __use(current[1]); + + return { data }; +} diff --git a/src/react/internal/cache/FragmentReference.ts b/src/react/internal/cache/FragmentReference.ts new file mode 100644 index 00000000000..85453892bcc --- /dev/null +++ b/src/react/internal/cache/FragmentReference.ts @@ -0,0 +1,200 @@ +import { equal } from "@wry/equality"; +import type { + WatchFragmentOptions, + WatchFragmentResult, +} from "../../../cache/index.js"; +import type { ApolloClient } from "../../../core/ApolloClient.js"; +import type { MaybeMasked } from "../../../masking/index.js"; +import { + createFulfilledPromise, + wrapPromiseWithState, +} from "../../../utilities/index.js"; +import type { + Observable, + ObservableSubscription, + PromiseWithState, +} from "../../../utilities/index.js"; +import type { FragmentKey } from "./types.js"; + +type FragmentRefPromise = PromiseWithState; +type Listener = (promise: FragmentRefPromise) => void; + +interface FragmentReferenceOptions { + autoDisposeTimeoutMs?: number; + onDispose?: () => void; +} + +export class FragmentReference< + TData = unknown, + TVariables = Record, +> { + public readonly observable: Observable>; + public readonly key: FragmentKey = {}; + public promise!: FragmentRefPromise>; + + private resolve: ((result: MaybeMasked) => void) | undefined; + private reject: ((error: unknown) => void) | undefined; + + private subscription!: ObservableSubscription; + private listeners = new Set>>(); + private autoDisposeTimeoutId?: NodeJS.Timeout; + + private references = 0; + + constructor( + client: ApolloClient, + watchFragmentOptions: WatchFragmentOptions & { + from: string; + }, + options: FragmentReferenceOptions + ) { + this.dispose = this.dispose.bind(this); + this.handleNext = this.handleNext.bind(this); + this.handleError = this.handleError.bind(this); + + this.observable = client.watchFragment(watchFragmentOptions); + + if (options.onDispose) { + this.onDispose = options.onDispose; + } + + const diff = this.getDiff(client, watchFragmentOptions); + + // Start a timer that will automatically dispose of the query if the + // suspended resource does not use this fragmentRef in the given time. This + // helps prevent memory leaks when a component has unmounted before the + // query has finished loading. + const startDisposeTimer = () => { + if (!this.references) { + this.autoDisposeTimeoutId = setTimeout( + this.dispose, + options.autoDisposeTimeoutMs ?? 30_000 + ); + } + }; + + this.promise = + diff.complete ? + createFulfilledPromise(diff.result) + : this.createPendingPromise(); + this.subscribeToFragment(); + + this.promise.then(startDisposeTimer, startDisposeTimer); + } + + listen(listener: Listener>) { + this.listeners.add(listener); + + return () => { + this.listeners.delete(listener); + }; + } + + retain() { + this.references++; + clearTimeout(this.autoDisposeTimeoutId); + let disposed = false; + + return () => { + if (disposed) { + return; + } + + disposed = true; + this.references--; + + setTimeout(() => { + if (!this.references) { + this.dispose(); + } + }); + }; + } + + private dispose() { + this.subscription.unsubscribe(); + this.onDispose(); + } + + private onDispose() { + // noop. overridable by options + } + + private subscribeToFragment() { + this.subscription = this.observable.subscribe( + this.handleNext.bind(this), + this.handleError.bind(this) + ); + } + + private handleNext(result: WatchFragmentResult) { + switch (this.promise.status) { + case "pending": { + if (result.complete) { + return this.resolve?.(result.data); + } + + this.deliver(this.promise); + break; + } + case "fulfilled": { + // This can occur when we already have a result written to the cache and + // we subscribe for the first time. We create a fulfilled promise in the + // constructor with a value that is the same as the first emitted value + // so we want to skip delivering it. + if (equal(this.promise.value, result.data)) { + return; + } + + this.promise = + result.complete ? + createFulfilledPromise(result.data) + : this.createPendingPromise(); + + this.deliver(this.promise); + } + } + } + + private handleError(error: unknown) { + this.reject?.(error); + } + + private deliver(promise: FragmentRefPromise>) { + this.listeners.forEach((listener) => listener(promise)); + } + + private createPendingPromise() { + return wrapPromiseWithState( + new Promise>((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }) + ); + } + + private getDiff( + client: ApolloClient, + options: WatchFragmentOptions & { from: string } + ) { + const { cache } = client; + const { from, fragment, fragmentName } = options; + + const diff = cache.diff({ + ...options, + query: cache["getFragmentDoc"](fragment, fragmentName), + returnPartialData: true, + id: from, + optimistic: true, + }); + + return { + ...diff, + result: client["queryManager"].maskFragment({ + fragment, + fragmentName, + data: diff.result, + }) as MaybeMasked, + }; + } +} diff --git a/src/react/internal/cache/SuspenseCache.ts b/src/react/internal/cache/SuspenseCache.ts index 8b1eba321b5..03bf0c43049 100644 --- a/src/react/internal/cache/SuspenseCache.ts +++ b/src/react/internal/cache/SuspenseCache.ts @@ -1,8 +1,13 @@ import { Trie } from "@wry/trie"; -import type { ObservableQuery } from "../../../core/index.js"; +import type { + ApolloClient, + ObservableQuery, + WatchFragmentOptions, +} from "../../../core/index.js"; import { canUseWeakMap } from "../../../utilities/index.js"; import { InternalQueryReference } from "./QueryReference.js"; -import type { CacheKey } from "./types.js"; +import type { CacheKey, FragmentCacheKey } from "./types.js"; +import { FragmentReference } from "./FragmentReference.js"; export interface SuspenseCacheOptions { /** @@ -22,6 +27,10 @@ export class SuspenseCache { private queryRefs = new Trie<{ current?: InternalQueryReference }>( canUseWeakMap ); + private fragmentRefs = new Trie<{ current?: FragmentReference }>( + canUseWeakMap + ); + private options: SuspenseCacheOptions; constructor(options: SuspenseCacheOptions = Object.create(null)) { @@ -48,6 +57,27 @@ export class SuspenseCache { return ref.current; } + getFragmentRef( + cacheKey: FragmentCacheKey, + client: ApolloClient, + options: WatchFragmentOptions & { from: string } + ) { + const ref = this.fragmentRefs.lookupArray(cacheKey) as { + current?: FragmentReference; + }; + + if (!ref.current) { + ref.current = new FragmentReference(client, options, { + autoDisposeTimeoutMs: this.options.autoDisposeTimeoutMs, + onDispose: () => { + delete ref.current; + }, + }); + } + + return ref.current; + } + add(cacheKey: CacheKey, queryRef: InternalQueryReference) { const ref = this.queryRefs.lookupArray(cacheKey); ref.current = queryRef; diff --git a/src/react/internal/cache/types.ts b/src/react/internal/cache/types.ts index 40f3c4cc8fc..a163431ad9d 100644 --- a/src/react/internal/cache/types.ts +++ b/src/react/internal/cache/types.ts @@ -6,6 +6,16 @@ export type CacheKey = [ ...queryKey: any[], ]; +export type FragmentCacheKey = [ + cacheId: string, + fragment: DocumentNode, + stringifiedVariables: string, +]; + export interface QueryKey { __queryKey?: string; } + +export interface FragmentKey { + __fragmentKey?: string; +} From cc47e1ccdd9579dc159e6ff9ad8d093d16f4180e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 5 Feb 2025 09:09:43 -0700 Subject: [PATCH 11/68] Update ROADMAP.md --- ROADMAP.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6fcfda717aa..f8dac57c1b7 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 🔮 Apollo Client Ecosystem Roadmap -**Last updated: 2025-01-22** +**Last updated: 2025-02-05** For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md). @@ -17,10 +17,11 @@ For up to date release notes, refer to the project's [Changelog](https://github. ### Apollo Client -#### 3.13.0 - February 3, 2024 -_Release candidate - January 27th_ +#### 3.13.0 - February 13, 2024 +_Release candidate - February 6th_ - `useSuspenseFragment` +- `onCompleted` and `onError` callback deprecations #### [4.0.0](https://github.com/apollographql/apollo-client/milestone/31) - TBD _Release candidate - TBD_ @@ -54,6 +55,6 @@ _These changes will take longer than anticipated due to prioritization on Apollo ### Apollo Client React Framework Integrations - New/more robust documentation -- Support for `@defer` with `PreloadQuery` +- Support for `@defer` with `PreloadQuery` (merged) - Support for Apollo Client Streaming in TanStack Router - Support for Apollo Client Streaming in React Router 7 (merged) From 219b26ba5a697981ad700e05b926d42db0fb8e59 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 5 Feb 2025 18:27:35 +0100 Subject: [PATCH 12/68] Add `graphql-ws` `^6.0.3` as a valid `peerDependency` (#12342) --- .changeset/lazy-clocks-sip.md | 5 +++++ package-lock.json | 32 ++++++++++++++++++++++---------- package.json | 4 ++-- 3 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 .changeset/lazy-clocks-sip.md diff --git a/.changeset/lazy-clocks-sip.md b/.changeset/lazy-clocks-sip.md new file mode 100644 index 00000000000..80810e8cb87 --- /dev/null +++ b/.changeset/lazy-clocks-sip.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Add `graphql-ws` `^6.0.3` as a valid `peerDependency` diff --git a/package-lock.json b/package-lock.json index b8452c3f72e..41da0d0073f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -83,7 +83,7 @@ "globals": "15.14.0", "graphql": "16.9.0", "graphql-17-alpha2": "npm:graphql@17.0.0-alpha.2", - "graphql-ws": "5.16.0", + "graphql-ws": "6.0.3", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-junit": "16.0.0", @@ -124,7 +124,7 @@ }, "peerDependencies": { "graphql": "^15.0.0 || ^16.0.0", - "graphql-ws": "^5.5.5", + "graphql-ws": "^5.5.5 || ^6.0.3", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" @@ -7997,18 +7997,30 @@ } }, "node_modules/graphql-ws": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.0.tgz", - "integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.3.tgz", + "integrity": "sha512-mvLRHihMg0llF74vo16063HufZHMGaiMxAjzyj0ARYueIikGzj1khlbPNl7vUc2h9rxbq9pGpQYbqypgq1fAXA==", "dev": true, - "workspaces": [ - "website" - ], + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=20" }, "peerDependencies": { - "graphql": ">=0.11 <=16" + "@fastify/websocket": "^10 || ^11", + "graphql": "^15.10.1 || ^16", + "uWebSockets.js": "^20", + "ws": "^8" + }, + "peerDependenciesMeta": { + "@fastify/websocket": { + "optional": true + }, + "uWebSockets.js": { + "optional": true + }, + "ws": { + "optional": true + } } }, "node_modules/has": { diff --git a/package.json b/package.json index de921768aa7..f082c732038 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ }, "peerDependencies": { "graphql": "^15.0.0 || ^16.0.0", - "graphql-ws": "^5.5.5", + "graphql-ws": "^5.5.5 || ^6.0.3", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" @@ -166,7 +166,7 @@ "globals": "15.14.0", "graphql": "16.9.0", "graphql-17-alpha2": "npm:graphql@17.0.0-alpha.2", - "graphql-ws": "5.16.0", + "graphql-ws": "6.0.3", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-junit": "16.0.0", From f2bb0b9955564e432345ee8bd431290e698d092c Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 5 Feb 2025 18:49:59 +0100 Subject: [PATCH 13/68] `useReadQuery`/`useQueryRefHandlers`: Fix a "hook order" warning that might be emitted in React 19 dev mode. (#12341) * `useReadQuery`/`useQueryRefHandlers`: Fix a "hook order" warning that might be emitted in React 19 dev mode. * change import to type * add two eslint-disables * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: phryneas <4282439+phryneas@users.noreply.github.com> --- .changeset/large-timers-lay.md | 5 +++++ .size-limits.json | 2 +- src/react/hooks/useQueryRefHandlers.ts | 22 +++++++++++----------- src/react/hooks/useReadQuery.ts | 26 ++++++++++++++------------ 4 files changed, 31 insertions(+), 24 deletions(-) create mode 100644 .changeset/large-timers-lay.md diff --git a/.changeset/large-timers-lay.md b/.changeset/large-timers-lay.md new file mode 100644 index 00000000000..8712178dbf1 --- /dev/null +++ b/.changeset/large-timers-lay.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`useReadQuery`/`useQueryRefHandlers`: Fix a "hook order" warning that might be emitted in React 19 dev mode. diff --git a/.size-limits.json b/.size-limits.json index a808e1e2da7..f7f8f2606dc 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41643, + "dist/apollo-client.min.cjs": 41642, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34384 } diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts index 8204e3e5df7..3de9ce5631f 100644 --- a/src/react/hooks/useQueryRefHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -16,6 +16,8 @@ import type { import type { FetchMoreQueryOptions } from "../../core/watchQueryOptions.js"; import { useApolloClient } from "./useApolloClient.js"; import { wrapHook } from "./internal/index.js"; +import type { ApolloClient } from "../../core/ApolloClient.js"; +import type { ObservableQuery } from "../../core/ObservableQuery.js"; export interface UseQueryRefHandlersResult< TData = unknown, @@ -55,21 +57,19 @@ export function useQueryRefHandlers< queryRef: QueryRef ): UseQueryRefHandlersResult { const unwrapped = unwrapQueryRef(queryRef); + const clientOrObsQuery = useApolloClient( + unwrapped ? + // passing an `ObservableQuery` is not supported by the types, but it will + // return any truthy value that is passed in as an override so we cast the result + (unwrapped["observable"] as any) + : undefined + ) as ApolloClient | ObservableQuery; return wrapHook( "useQueryRefHandlers", + // eslint-disable-next-line react-compiler/react-compiler useQueryRefHandlers_, - unwrapped ? - unwrapped["observable"] - // in the case of a "transported" queryRef object, we need to use the - // client that's available to us at the current position in the React tree - // that ApolloClient will then have the job to recreate a real queryRef from - // the transported object - // This is just a context read - it's fine to do this conditionally. - // This hook wrapper also shouldn't be optimized by React Compiler. - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/rules-of-hooks - : useApolloClient() + clientOrObsQuery )(queryRef); } diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 5c444e0eb7c..40e3f02e0d0 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -10,7 +10,11 @@ import { __use, wrapHook } from "./internal/index.js"; import { toApolloError } from "./useSuspenseQuery.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import type { ApolloError } from "../../errors/index.js"; -import type { NetworkStatus } from "../../core/index.js"; +import type { + ApolloClient, + NetworkStatus, + ObservableQuery, +} from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; import type { MaybeMasked } from "../../masking/index.js"; @@ -43,21 +47,19 @@ export function useReadQuery( queryRef: QueryRef ): UseReadQueryResult { const unwrapped = unwrapQueryRef(queryRef); + const clientOrObsQuery = useApolloClient( + unwrapped ? + // passing an `ObservableQuery` is not supported by the types, but it will + // return any truthy value that is passed in as an override so we cast the result + (unwrapped["observable"] as any) + : undefined + ) as ApolloClient | ObservableQuery; return wrapHook( "useReadQuery", + // eslint-disable-next-line react-compiler/react-compiler useReadQuery_, - unwrapped ? - unwrapped["observable"] - // in the case of a "transported" queryRef object, we need to use the - // client that's available to us at the current position in the React tree - // that ApolloClient will then have the job to recreate a real queryRef from - // the transported object - // This is just a context read - it's fine to do this conditionally. - // This hook wrapper also shouldn't be optimized by React Compiler. - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/rules-of-hooks - : useApolloClient() + clientOrObsQuery )(queryRef); } From 50318372f542f07c9ade69c780b3b2695147ba93 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 01:37:32 +0000 Subject: [PATCH 14/68] Prepare for beta release --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index ba55303eb3c..54334695736 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,6 +1,6 @@ { "mode": "pre", - "tag": "rc", + "tag": "beta", "initialVersions": { "@apollo/client": "3.12.2" }, From 670f112a7d9d85cb357eb279a488ac2c6d0137a9 Mon Sep 17 00:00:00 2001 From: "Michael \"Mike\" Ferris" Date: Wed, 5 Feb 2025 20:44:31 -0500 Subject: [PATCH 15/68] Provide type-safe solution for updateQuery previous data (#12276) --- .api-reports/api-report-core.api.md | 60 +++++++++---- .api-reports/api-report-react.api.md | 73 ++++++++++----- .../api-report-react_components.api.md | 69 ++++++++++---- .api-reports/api-report-react_context.api.md | 69 ++++++++++---- .api-reports/api-report-react_hoc.api.md | 62 ++++++++----- .api-reports/api-report-react_hooks.api.md | 73 ++++++++++----- .api-reports/api-report-react_internal.api.md | 73 ++++++++++----- .api-reports/api-report-react_ssr.api.md | 69 ++++++++++---- .api-reports/api-report-testing.api.md | 58 ++++++++---- .api-reports/api-report-testing_core.api.md | 58 ++++++++---- .api-reports/api-report-utilities.api.md | 58 ++++++++---- .api-reports/api-report.api.md | 68 +++++++++----- .changeset/bright-guests-chew.md | 16 ++++ .changeset/pretty-planets-cough.md | 24 +++++ .changeset/tough-years-destroy.md | 5 ++ .size-limits.json | 4 +- .vscode/settings.json | 3 + src/__tests__/ApolloClient.ts | 34 ++++--- src/__tests__/dataMasking.ts | 57 ++++++++++-- src/__tests__/subscribeToMore.ts | 13 ++- src/core/ObservableQuery.ts | 42 +++++---- src/core/__tests__/ObservableQuery.ts | 64 +++++++++++++ src/core/index.ts | 9 +- src/core/watchQueryOptions.ts | 90 +++++++++++++++++-- src/react/hoc/types.ts | 13 ++- .../__tests__/useBackgroundQuery.test.tsx | 72 +++++++++++++-- .../hooks/__tests__/useLazyQuery.test.tsx | 55 ++++++++++-- .../hooks/__tests__/useLoadableQuery.test.tsx | 9 +- .../__tests__/useQueryRefHandlers.test.tsx | 18 ++-- .../hooks/__tests__/useSuspenseQuery.test.tsx | 24 ++++- src/react/hooks/useBackgroundQuery.ts | 11 ++- src/react/hooks/useLoadableQuery.ts | 15 ++-- src/react/hooks/useQuery.ts | 9 +- src/react/hooks/useQueryRefHandlers.ts | 11 ++- src/react/hooks/useSuspenseQuery.ts | 10 +-- src/react/types/types.ts | 21 +---- 36 files changed, 1063 insertions(+), 356 deletions(-) create mode 100644 .changeset/bright-guests-chew.md create mode 100644 .changeset/pretty-planets-cough.md create mode 100644 .changeset/tough-years-destroy.md diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 53a5e866698..017e6932435 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1726,8 +1726,8 @@ export class ObservableQuery>): void; startPolling(pollInterval: number): void; stopPolling(): void; - subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -2317,12 +2317,33 @@ class Stump extends Layer { } // @public (undocumented) -export type SubscribeToMoreOptions = { +export interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} + +// @public (undocumented) +export interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +export type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -2421,18 +2442,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +export interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} // @public (undocumented) -export interface UpdateQueryOptions { - // (undocumented) +export type UpdateQueryOptions = { variables?: TVariables; -} +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) export interface UriFunction { @@ -2508,11 +2533,10 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:162: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:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277: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.api.md b/.api-reports/api-report-react.api.md index f2da5216c6e..09cf76aaa3c 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1434,8 +1434,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1452,8 +1453,9 @@ export interface ObservableQueryFields>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -2093,15 +2095,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreFunction = ObservableQueryFields["subscribeToMore"]; +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: Context; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: Context; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -2223,12 +2245,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -2557,16 +2589,15 @@ interface WatchQueryOptions(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1325,8 +1326,9 @@ interface ObservableQueryFields { reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -1839,12 +1841,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} + +// @public (undocumented) +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public @deprecated (undocumented) @@ -1951,12 +1976,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -2002,13 +2037,13 @@ interface WatchQueryOptions(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1278,8 +1279,9 @@ interface ObservableQueryFields { reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -1791,12 +1793,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} + +// @public (undocumented) +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1870,12 +1895,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -1921,13 +1956,13 @@ interface WatchQueryOptions(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1387,10 +1388,8 @@ export interface QueryControls void; // (undocumented) subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - // Warning: (ae-forgotten-export) The symbol "UpdateQueryOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) - updateQuery: (mapFn: (previousQueryResult: any, options: UpdateQueryOptions) => any) => void; + updateQuery: (mapFn: UpdateQueryMapFn) => void; // (undocumented) variables: TGraphQLVariables; } @@ -1796,12 +1795,29 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1875,18 +1891,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} // @public (undocumented) -interface UpdateQueryOptions { - // (undocumented) +type UpdateQueryOptions = { variables?: TVariables; -} +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -1949,13 +1969,13 @@ export function withSubscription(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1396,8 +1397,9 @@ interface ObservableQueryFields { reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -1929,15 +1931,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreFunction = ObservableQueryFields["subscribeToMore"]; +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // Warning: (ae-forgotten-export) The symbol "BaseSubscriptionOptions" needs to be exported by the entry point index.d.ts @@ -2046,12 +2068,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -2381,16 +2413,15 @@ interface WatchQueryOptions(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1433,8 +1434,9 @@ interface ObservableQueryFields { reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -2029,15 +2031,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreFunction = ObservableQueryFields["subscribeToMore"]; +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -2161,12 +2183,22 @@ export function unwrapQueryRef(queryRef: Partial>) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) export function updateWrappedQueryRef(queryRef: WrappedQueryRef, promise: QueryRefPromise): void; @@ -2506,16 +2538,15 @@ export function wrapQueryRef(inter // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105: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:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:38:3 - (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:54:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:78:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:51:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:75:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useSuspenseFragment.ts:60:5 - (ae-forgotten-export) The symbol "From" needs to be exported by the entry point index.d.ts // src/react/query-preloader/createQueryPreloader.ts:145:3 - (ae-forgotten-export) The symbol "PreloadQueryFetchPolicy" needs to be exported by the entry point index.d.ts // src/react/query-preloader/createQueryPreloader.ts:167:5 - (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index 9c6b605a377..387a028e627 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1245,8 +1245,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1263,8 +1264,9 @@ interface ObservableQueryFields { reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -1776,12 +1778,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} + +// @public (undocumented) +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1855,12 +1880,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -1906,13 +1941,13 @@ interface WatchQueryOptions(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1824,12 +1825,29 @@ type StoreValue = number | string | string[] | Reference | Reference[] | null | export function subscribeAndCount(reject: (reason: any) => any, observable: Observable, cb: (handleCount: number, result: TResult) => any): Subscription; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1906,12 +1924,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -1974,13 +2002,13 @@ export function withWarningSpy(it: (...args: TArgs // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105: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:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" 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.api.md b/.api-reports/api-report-testing_core.api.md index 82e35138da4..fa5b233b320 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1331,8 +1331,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1781,12 +1782,29 @@ type StoreValue = number | string | string[] | Reference | Reference[] | null | export function subscribeAndCount(reject: (reason: any) => any, observable: Observable, cb: (handleCount: number, result: TResult) => any): Subscription; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1863,12 +1881,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -1931,13 +1959,13 @@ export function withWarningSpy(it: (...args: TArgs // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105: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:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" 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.api.md b/.api-reports/api-report-utilities.api.md index c8a866c726d..59b4ba4b1ba 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -2020,8 +2020,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -2652,12 +2653,29 @@ class Stump extends Layer { } // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -2782,12 +2800,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -2876,13 +2904,13 @@ 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:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" 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.api.md b/.api-reports/api-report.api.md index cfb98c0fbf3..64fddeb0377 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -1943,8 +1943,8 @@ export class ObservableQuery>): void; startPolling(pollInterval: number): void; stopPolling(): void; - subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1961,8 +1961,8 @@ export interface ObservableQueryFields>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -2735,15 +2735,33 @@ class Stump extends Layer { } // @public (undocumented) -type SubscribeToMoreFunction = ObservableQueryFields["subscribeToMore"]; +export interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} // @public (undocumented) -export type SubscribeToMoreOptions = { +export interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +export type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -2890,18 +2908,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +export interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} // @public (undocumented) -export interface UpdateQueryOptions { - // (undocumented) +export type UpdateQueryOptions = { variables?: TVariables; -} +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) export interface UriFunction { @@ -3257,15 +3279,13 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:162: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:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277: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:38:3 - (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:54:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:78:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:51:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:75:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useLoadableQuery.ts:120:9 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useSuspenseFragment.ts:60:5 - (ae-forgotten-export) The symbol "From" needs to be exported by the entry point index.d.ts diff --git a/.changeset/bright-guests-chew.md b/.changeset/bright-guests-chew.md new file mode 100644 index 00000000000..2d4c565a396 --- /dev/null +++ b/.changeset/bright-guests-chew.md @@ -0,0 +1,16 @@ +--- +"@apollo/client": patch +--- + +Fix the return type of the `updateQuery` function to allow for `undefined`. `updateQuery` had the ability to bail out of the update by returning a falsey value, but the return type enforced a query value. + +```ts +observableQuery.updateQuery((unsafePreviousData, { previousData, complete }) => { + if (!complete) { + // Bail out of the update by returning early + return; + } + + // ... +}); +``` diff --git a/.changeset/pretty-planets-cough.md b/.changeset/pretty-planets-cough.md new file mode 100644 index 00000000000..b7b76ba4aa6 --- /dev/null +++ b/.changeset/pretty-planets-cough.md @@ -0,0 +1,24 @@ +--- +"@apollo/client": minor +--- + +Provide a more type-safe option for the previous data value passed to `observableQuery.updateQuery`. Using it could result in crashes at runtime as this callback could be called with partial data even though its type reported the value as a complete result. + +The `updateQuery` callback function is now called with a new type-safe `previousData` property and a new `complete` property in the 2nd argument that determines whether `previousData` is a complete or partial result. + +As a result of this change, it is recommended to use the `previousData` property passed to the 2nd argument of the callback rather than using the previous data value from the first argument since that value is not type-safe. The first argument is now deprecated and will be removed in a future version of Apollo Client. + +```ts +observableQuery.updateQuery((unsafePreviousData, { previousData, complete }) => { + previousData + // ^? TData | DeepPartial | undefined + + if (complete) { + previousData + // ^? TData + } else { + previousData + // ^? DeepPartial | undefined + } +}) +``` diff --git a/.changeset/tough-years-destroy.md b/.changeset/tough-years-destroy.md new file mode 100644 index 00000000000..90dfab53bb8 --- /dev/null +++ b/.changeset/tough-years-destroy.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix the type of the `variables` property passed as the 2nd argument to the `subscribeToMore` callback. This was previously reported as the `variables` type for the subscription itself, but is now properly typed as the query `variables`. diff --git a/.size-limits.json b/.size-limits.json index 4d4b64c5f17..c7ac8973041 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42176, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34384 + "dist/apollo-client.min.cjs": 42196, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34405 } diff --git a/.vscode/settings.json b/.vscode/settings.json index c2580b79482..604746d4cfa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,9 @@ "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "typescript.tsdk": "node_modules/typescript/lib", + "editor.codeActionsOnSave": { + "source.organizeImports": "never" + }, "cSpell.enableFiletypes": ["mdx"], "jest.jestCommandLine": "node --expose-gc node_modules/.bin/jest --config ./config/jest.config.js --ignoreProjects 'ReactDOM 17' --runInBand" } diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 72d551c4b9b..fce6521d2af 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -11,7 +11,7 @@ import { } from "../core"; import { Kind } from "graphql"; -import { Observable } from "../utilities"; +import { DeepPartial, Observable } from "../utilities"; import { ApolloLink, FetchResult } from "../link/core"; import { HttpLink } from "../link/http"; import { createFragmentRegistry, InMemoryCache } from "../cache"; @@ -3218,25 +3218,39 @@ describe("ApolloClient", () => { UnmaskedQuery | undefined >(); - observableQuery.updateQuery((previousData) => { - expectTypeOf(previousData).toMatchTypeOf(); - expectTypeOf(previousData).not.toMatchTypeOf(); + observableQuery.updateQuery( + (_previousData, { complete, previousData }) => { + expectTypeOf(_previousData).toEqualTypeOf(); + expectTypeOf(_previousData).not.toMatchTypeOf(); - return {} as UnmaskedQuery; - }); + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + } + ); observableQuery.subscribeToMore({ document: subscription, - updateQuery(queryData, { subscriptionData }) { - expectTypeOf(queryData).toMatchTypeOf(); + updateQuery(queryData, { subscriptionData, complete, previousData }) { + expectTypeOf(queryData).toEqualTypeOf(); expectTypeOf(queryData).not.toMatchTypeOf(); + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + expectTypeOf( subscriptionData.data ).toMatchTypeOf(); expectTypeOf(subscriptionData.data).not.toMatchTypeOf(); - - return {} as UnmaskedQuery; }, }); }); diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index 9c23822916b..8c386b67e6f 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -1891,8 +1891,16 @@ describe("client.watchQuery", () => { } const updateQuery: Parameters[0] = jest.fn( - (previousResult) => { - return { user: { ...previousResult.user, name: "User (updated)" } }; + (previousResult, { complete, previousData }) => { + expect(complete).toBe(true); + expect(previousData).toStrictEqual(previousResult); + // Type Guard + if (!complete) { + return; + } + return { + user: { ...previousData.user, name: "User (updated)" }, + }; } ); @@ -1900,7 +1908,13 @@ describe("client.watchQuery", () => { expect(updateQuery).toHaveBeenCalledWith( { user: { __typename: "User", id: 1, name: "User 1", age: 30 } }, - { variables: { id: 1 } } + { + variables: { id: 1 }, + complete: true, + previousData: { + user: { __typename: "User", id: 1, name: "User 1", age: 30 }, + }, + } ); { @@ -4831,7 +4845,16 @@ describe("observableQuery.subscribeToMore", () => { }, }, { + complete: true, variables: {}, + previousData: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, subscriptionData: { data: { addedComment: { @@ -4958,7 +4981,16 @@ describe("observableQuery.subscribeToMore", () => { }, }, { + complete: true, variables: {}, + previousData: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, subscriptionData: { data: { addedComment: { @@ -5006,8 +5038,8 @@ describe("observableQuery.subscribeToMore", () => { `; const subscription = gql` - subscription NewCommentSubscription { - addedComment { + subscription NewCommentSubscription($id: ID!) { + addedComment(id: $id) { id ...CommentFields } @@ -5060,7 +5092,11 @@ describe("observableQuery.subscribeToMore", () => { return { recentComment: subscriptionData.data.addedComment }; }); - observable.subscribeToMore({ document: subscription, updateQuery }); + observable.subscribeToMore({ + document: subscription, + updateQuery, + variables: { id: 1 }, + }); subscriptionLink.simulateResult({ result: { @@ -5087,7 +5123,16 @@ describe("observableQuery.subscribeToMore", () => { }, }, { + complete: true, variables: {}, + previousData: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, subscriptionData: { data: { addedComment: { diff --git a/src/__tests__/subscribeToMore.ts b/src/__tests__/subscribeToMore.ts index 6216eaa85c9..9761ee3f5a6 100644 --- a/src/__tests__/subscribeToMore.ts +++ b/src/__tests__/subscribeToMore.ts @@ -240,10 +240,17 @@ describe("subscribeToMore", () => { name } `, - updateQuery: (prev, { subscriptionData }) => { - expect(prev.entry).not.toContainEqual(nextMutation); + updateQuery: (prev, { subscriptionData, complete, previousData }) => { + expect(complete).toBe(true); + expect(previousData).toStrictEqual(prev); + // Type Guard + if (!complete) { + return; + } + + expect(previousData.entry).not.toContainEqual(nextMutation); return { - entry: [...prev.entry, { value: subscriptionData.data.name }], + entry: [...previousData.entry, { value: subscriptionData.data.name }], }; }, }); diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 2a70e6e9097..c3109810b8e 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -31,6 +31,8 @@ import type { SubscribeToMoreOptions, NextFetchPolicyContext, WatchQueryFetchPolicy, + UpdateQueryMapFn, + UpdateQueryOptions, } from "./watchQueryOptions.js"; import type { QueryInfo } from "./QueryInfo.js"; import type { MissingFieldError } from "../cache/index.js"; @@ -54,10 +56,6 @@ export interface FetchMoreOptions< ) => TData; } -export interface UpdateQueryOptions { - variables?: TVariables; -} - interface Last { result: ApolloQueryResult; variables?: TVariables; @@ -619,9 +617,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, options: SubscribeToMoreOptions< TData, TSubscriptionVariables, - TSubscriptionData + TSubscriptionData, + TVariables > - ) { + ): () => void { const subscription = this.queryManager .startGraphQLSubscription({ query: options.document, @@ -632,12 +631,11 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, next: (subscriptionData: { data: Unmasked }) => { const { updateQuery } = options; if (updateQuery) { - this.updateQuery( - (previous, { variables }) => - updateQuery(previous, { - subscriptionData, - variables, - }) + this.updateQuery((previous, updateOptions) => + updateQuery(previous, { + subscriptionData, + ...updateOptions, + }) ); } }, @@ -722,23 +720,23 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, * * 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: Unmasked, - options: Pick, "variables"> - ) => Unmasked - ): void { + public updateQuery(mapFn: UpdateQueryMapFn): void { const { queryManager } = this; - const { result } = queryManager.cache.diff({ + const { result, complete } = queryManager.cache.diff({ query: this.options.query, variables: this.variables, returnPartialData: true, optimistic: false, }); - const newResult = mapFn(result! as Unmasked, { - variables: (this as any).variables, - }); + const newResult = mapFn( + result! as Unmasked, + { + variables: this.variables, + complete: !!complete, + previousData: result, + } as UpdateQueryOptions + ); if (newResult) { queryManager.cache.writeQuery({ diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index 133165db818..c816957a898 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -12,6 +12,7 @@ import { ObservableQuery } from "../ObservableQuery"; import { QueryManager } from "../QueryManager"; import { + DeepPartial, DocumentTransform, Observable, removeDirectivesFromDocument, @@ -21,6 +22,7 @@ import { InMemoryCache } from "../../cache"; import { ApolloError } from "../../errors"; import { MockLink, MockSubscriptionLink, tick, wait } from "../../testing"; +import { expectTypeOf } from "expect-type"; import { SubscriptionObserver } from "zen-observable-ts"; import { waitFor } from "@testing-library/react"; @@ -3350,6 +3352,68 @@ describe("ObservableQuery", () => { }); }); + describe("updateQuery", () => { + it("should be able to determine if the previous result is complete", async () => { + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + ]), + }); + + const observable = client.watchQuery({ + query, + variables, + }); + + let updateQuerySpy = jest.fn(); + observable.updateQuery((previous, { complete, previousData }) => { + updateQuerySpy(); + expect(previous).toEqual({}); + expect(complete).toBe(false); + expect(previousData).toStrictEqual(previous); + + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + }); + + observable.subscribe(jest.fn()); + + await waitFor(() => { + expect(observable.getCurrentResult(false)).toEqual({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); + + observable.updateQuery((previous, { complete, previousData }) => { + updateQuerySpy(); + expect(previous).toEqual(dataOne); + expect(complete).toBe(true); + expect(previousData).toStrictEqual(previous); + + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + }); + + expect(updateQuerySpy).toHaveBeenCalledTimes(2); + }); + }); + it("QueryInfo does not notify for !== but deep-equal results", async () => { const client = new ApolloClient({ cache: new InMemoryCache({ addTypename: false }), diff --git a/src/core/index.ts b/src/core/index.ts index 3b0143ae6c1..9c6ac111d18 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -2,10 +2,7 @@ export type { ApolloClientOptions, DefaultOptions } from "./ApolloClient.js"; export { ApolloClient, mergeOptions } from "./ApolloClient.js"; -export type { - FetchMoreOptions, - UpdateQueryOptions, -} from "./ObservableQuery.js"; +export type { FetchMoreOptions } from "./ObservableQuery.js"; export { ObservableQuery } from "./ObservableQuery.js"; export type { QueryOptions, @@ -19,6 +16,10 @@ export type { ErrorPolicy, FetchMoreQueryOptions, SubscribeToMoreOptions, + SubscribeToMoreFunction, + UpdateQueryMapFn, + UpdateQueryOptions, + SubscribeToMoreUpdateQueryFn, } from "./watchQueryOptions.js"; export { NetworkStatus, isNetworkRequestSettled } from "./networkStatus.js"; export type * from "./types.js"; diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index 1528b1d0330..f03448a73a0 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -14,7 +14,7 @@ import type { ApolloCache } from "../cache/index.js"; import type { ObservableQuery } from "./ObservableQuery.js"; import type { IgnoreModifier } from "../cache/core/types/common.js"; import type { Unmasked } from "../masking/index.js"; -import type { NoInfer } from "../utilities/index.js"; +import type { DeepPartial, NoInfer } from "../utilities/index.js"; /** * fetchPolicy determines where the client may return a result from. The options are: @@ -164,6 +164,11 @@ export interface FetchMoreQueryOptions { context?: DefaultContext; } +/** + * @deprecated `UpdateQueryFn` is deprecated and will be removed or updated in a + * future version of Apollo Client. Use `SubscribeToMoreUpdateQueryFn` instead + * which provides a more type-safe result. + */ export type UpdateQueryFn< TData = any, TSubscriptionVariables = OperationVariables, @@ -176,19 +181,94 @@ export type UpdateQueryFn< } ) => Unmasked; -export type SubscribeToMoreOptions< +export type UpdateQueryOptions = { + variables?: TVariables; +} & ( + | { + /** + * Indicate if the previous query result has been found fully in the cache. + */ + complete: true; + previousData: Unmasked; + } + | { + /** + * Indicate if the previous query result has not been found fully in the cache. + * Might have partial or missing data. + */ + complete: false; + previousData: DeepPartial> | undefined; + } +); + +export interface UpdateQueryMapFn< TData = any, - TSubscriptionVariables = OperationVariables, + TVariables = OperationVariables, +> { + ( + /** + * @deprecated This value is not type-safe and may contain partial data. This + * argument will be removed in the next major version of Apollo Client. Use + * `options.previousData` instead for a more type-safe value. + */ + unsafePreviousData: Unmasked, + options: UpdateQueryOptions + ): Unmasked | void; +} + +export type SubscribeToMoreUpdateQueryFn< + TData = any, + TVariables extends OperationVariables = OperationVariables, TSubscriptionData = TData, > = { + ( + /** + * @deprecated This value is not type-safe and may contain partial data. This + * argument will be removed in the next major version of Apollo Client. Use + * `options.previousData` instead for a more type-safe value. + */ + unsafePreviousData: Unmasked, + options: UpdateQueryOptions & { + subscriptionData: { data: Unmasked }; + } + ): Unmasked | void; +}; + +export interface SubscribeToMoreOptions< + TData = any, + TSubscriptionVariables extends OperationVariables = OperationVariables, + TSubscriptionData = TData, + TVariables extends OperationVariables = TSubscriptionVariables, +> { document: | DocumentNode | TypedDocumentNode; variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + updateQuery?: SubscribeToMoreUpdateQueryFn< + TData, + TVariables, + TSubscriptionData + >; onError?: (error: Error) => void; context?: DefaultContext; -}; +} + +export interface SubscribeToMoreFunction< + TData, + TVariables extends OperationVariables = OperationVariables, +> { + < + TSubscriptionData = TData, + TSubscriptionVariables extends OperationVariables = TVariables, + >( + options: SubscribeToMoreOptions< + TData, + TSubscriptionVariables, + TSubscriptionData, + TVariables + > + ): () => void; +} export interface SubscriptionOptions< TVariables = OperationVariables, diff --git a/src/react/hoc/types.ts b/src/react/hoc/types.ts index 0d67f4c1ca0..892727c0db7 100644 --- a/src/react/hoc/types.ts +++ b/src/react/hoc/types.ts @@ -1,13 +1,14 @@ -import type { ApolloCache, ApolloClient } from "../../core/index.js"; import type { ApolloError } from "../../errors/index.js"; import type { + ApolloCache, + ApolloClient, ApolloQueryResult, - OperationVariables, + DefaultContext, FetchMoreOptions, - UpdateQueryOptions, FetchMoreQueryOptions, + OperationVariables, SubscribeToMoreOptions, - DefaultContext, + UpdateQueryMapFn, } from "../../core/index.js"; import type { MutationFunction, @@ -32,9 +33,7 @@ export interface QueryControls< startPolling: (pollInterval: number) => void; stopPolling: () => void; subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: ( - mapFn: (previousQueryResult: any, options: UpdateQueryOptions) => any - ) => void; + updateQuery: (mapFn: UpdateQueryMapFn) => void; } export type DataValue< diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index a335ede0877..5e788e03b56 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -42,6 +42,7 @@ import equal from "@wry/equality"; import { RefetchWritePolicy, SubscribeToMoreOptions, + SubscribeToMoreFunction, } from "../../../core/watchQueryOptions"; import { skipToken } from "../constants"; import { @@ -57,7 +58,6 @@ import { spyOnConsole, addDelayToMocks, } from "../../../testing/internal"; -import { SubscribeToMoreFunction } from "../useSuspenseQuery"; import { MaskedVariablesCaseData, setupMaskedVariablesCase, @@ -7190,6 +7190,8 @@ describe("fetchMore", () => { expect(updateQuery).toHaveBeenCalledWith( { greeting: "Hello" }, { + complete: true, + previousData: { greeting: "Hello" }, subscriptionData: { data: { greetingUpdated: "Subscription hello" }, }, @@ -8364,9 +8366,12 @@ describe.skip("type tests", () => { { const [, { subscribeToMore }] = useBackgroundQuery(query); - const subscription: MaskedDocumentNode = gql` - subscription { - pushLetter { + const subscription: MaskedDocumentNode< + Subscription, + { letterId: string } + > = gql` + subscription LetterPushed($letterId: ID!) { + pushLetter(letterId: $letterId) { id ...CharacterFragment } @@ -8379,15 +8384,41 @@ describe.skip("type tests", () => { subscribeToMore({ document: subscription, - updateQuery: (queryData, { subscriptionData }) => { + updateQuery: ( + queryData, + { subscriptionData, variables, complete, previousData } + ) => { expectTypeOf(queryData).toEqualTypeOf(); expectTypeOf(queryData).not.toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + | UnmaskedVariablesCaseData + | DeepPartial + | undefined + >(); + + if (complete) { + // Should narrow the type + expectTypeOf( + previousData + ).toEqualTypeOf(); + expectTypeOf( + previousData + ).not.toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } expectTypeOf( subscriptionData.data ).toEqualTypeOf(); expectTypeOf(subscriptionData.data).not.toEqualTypeOf(); + expectTypeOf(variables).toEqualTypeOf< + VariablesCaseVariables | undefined + >(); + return {} as UnmaskedVariablesCaseData; }, }); @@ -8411,16 +8442,43 @@ describe.skip("type tests", () => { subscribeToMore({ document: subscription, - updateQuery: (queryData, { subscriptionData }) => { + updateQuery: ( + queryData, + { subscriptionData, variables, complete, previousData } + ) => { expectTypeOf(queryData).toEqualTypeOf(); expectTypeOf(queryData).not.toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + | UnmaskedVariablesCaseData + | DeepPartial + | undefined + >(); + + if (complete) { + // Should narrow the type + expectTypeOf( + previousData + ).toEqualTypeOf(); + expectTypeOf( + previousData + ).not.toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + expectTypeOf( subscriptionData.data ).toEqualTypeOf(); expectTypeOf(subscriptionData.data).not.toEqualTypeOf(); - return {} as UnmaskedVariablesCaseData; + expectTypeOf(variables).toEqualTypeOf< + VariablesCaseVariables | undefined + >(); + + return queryData; }, }); } diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx index 802638fef98..a148a8256bc 100644 --- a/src/react/hooks/__tests__/useLazyQuery.test.tsx +++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx @@ -12,7 +12,7 @@ import { NetworkStatus, TypedDocumentNode, } from "../../../core"; -import { Observable } from "../../../utilities"; +import { DeepPartial, Observable } from "../../../utilities"; import { ApolloProvider } from "../../../react"; import { MockedProvider, @@ -3351,8 +3351,20 @@ describe.skip("Type Tests", () => { subscribeToMore({ document: gql`` as TypedDocumentNode, - updateQuery(queryData, { subscriptionData }) { + updateQuery(queryData, { subscriptionData, complete, previousData }) { expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf(complete).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + UnmaskedQuery | DeepPartial | undefined + >(); + + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } expectTypeOf( subscriptionData.data ).toEqualTypeOf(); @@ -3361,8 +3373,12 @@ describe.skip("Type Tests", () => { }, }); - updateQuery((previousData) => { - expectTypeOf(previousData).toEqualTypeOf(); + updateQuery((_previousData, { complete, previousData }) => { + expectTypeOf(_previousData).toEqualTypeOf(); + expectTypeOf(complete).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + UnmaskedQuery | DeepPartial | undefined + >(); return {} as UnmaskedQuery; }); @@ -3450,20 +3466,41 @@ describe.skip("Type Tests", () => { subscribeToMore({ document: gql`` as TypedDocumentNode, - updateQuery(queryData, { subscriptionData }) { + updateQuery(queryData, { subscriptionData, complete, previousData }) { expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + UnmaskedQuery | DeepPartial | undefined + >(); expectTypeOf( subscriptionData.data ).toEqualTypeOf(); + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + return {} as UnmaskedQuery; }, }); - updateQuery((previousData) => { - expectTypeOf(previousData).toEqualTypeOf(); - - return {} as UnmaskedQuery; + updateQuery((_previousData, { complete, previousData }) => { + expectTypeOf(_previousData).toEqualTypeOf(); + expectTypeOf(complete).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + UnmaskedQuery | DeepPartial | undefined + >(); + + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } }); { diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 86227f9775c..764edd1be8f 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -18,6 +18,7 @@ import { SubscribeToMoreOptions, split, } from "../../../core"; +import { SubscribeToMoreFunction } from "../../../core/watchQueryOptions"; import { MockedProvider, MockedProviderProps, @@ -39,11 +40,7 @@ import { ApolloProvider } from "../../context"; import { InMemoryCache } from "../../../cache"; import { LoadableQueryHookFetchPolicy } from "../../types/types"; import { QueryRef } from "../../../react"; -import { - FetchMoreFunction, - RefetchFunction, - SubscribeToMoreFunction, -} from "../useSuspenseQuery"; +import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import invariant, { InvariantError } from "ts-invariant"; import { SimpleCaseData, @@ -5097,6 +5094,8 @@ it("can subscribe to subscriptions and react to cache updates via `subscribeToMo expect(updateQuery).toHaveBeenCalledWith( { greeting: "Hello" }, { + complete: true, + previousData: { greeting: "Hello" }, subscriptionData: { data: { greetingUpdated: "Subscription hello" }, }, diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index 48a8ac2ba64..f0db8609db3 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -4,11 +4,14 @@ import { ApolloClient, InMemoryCache, NetworkStatus, - SubscribeToMoreOptions, TypedDocumentNode, gql, split, } from "../../../core"; +import { + SubscribeToMoreFunction, + SubscribeToMoreUpdateQueryFn, +} from "../../../core/watchQueryOptions"; import { MockLink, MockSubscriptionLink, @@ -23,7 +26,6 @@ import { } from "../../../testing/internal"; import { useQueryRefHandlers } from "../useQueryRefHandlers"; import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; -import type { SubscribeToMoreFunction } from "../useSuspenseQuery"; import { Suspense } from "react"; import { createQueryPreloader } from "../../query-preloader/createQueryPreloader"; import userEvent from "@testing-library/user-event"; @@ -1960,12 +1962,10 @@ test("can subscribe to subscriptions and react to cache updates via `subscribeTo greetingUpdated: string; } - type UpdateQueryFn = NonNullable< - SubscribeToMoreOptions< - SimpleCaseData, - Record, - SubscriptionData - >["updateQuery"] + type UpdateQueryFn = SubscribeToMoreUpdateQueryFn< + SimpleCaseData, + Record, + SubscriptionData >; const subscription: TypedDocumentNode< @@ -2092,6 +2092,8 @@ test("can subscribe to subscriptions and react to cache updates via `subscribeTo expect(updateQuery).toHaveBeenCalledWith( { greeting: "Hello" }, { + complete: true, + previousData: { greeting: "Hello" }, subscriptionData: { data: { greetingUpdated: "Subscription hello" }, }, diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index b7ba7bc04f1..c2e756ee15f 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -9636,6 +9636,8 @@ describe("useSuspenseQuery", () => { expect(updateQuery).toHaveBeenCalledWith( { greeting: "Hello" }, { + complete: true, + previousData: { greeting: "Hello" }, subscriptionData: { data: { greetingUpdated: "Subscription hello" }, }, @@ -12939,12 +12941,32 @@ describe("useSuspenseQuery", () => { subscribeToMore({ document: subscription, - updateQuery: (queryData, { subscriptionData }) => { + updateQuery: ( + queryData, + { subscriptionData, complete, previousData } + ) => { expectTypeOf(queryData).toEqualTypeOf(); expectTypeOf( queryData ).not.toEqualTypeOf(); + expectTypeOf(complete).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + | UnmaskedVariablesCaseData + | DeepPartial + | undefined + >(); + + if (complete) { + expectTypeOf( + previousData + ).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + expectTypeOf( subscriptionData.data ).toEqualTypeOf(); diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 40480fa0a02..256d3175c90 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -6,6 +6,7 @@ import type { TypedDocumentNode, WatchQueryOptions, } from "../../core/index.js"; +import type { SubscribeToMoreFunction } from "../../core/watchQueryOptions.js"; import { useApolloClient } from "./useApolloClient.js"; import { getSuspenseCache, @@ -17,11 +18,7 @@ import type { CacheKey, QueryRef } from "../internal/index.js"; import type { BackgroundQueryHookOptions, NoInfer } from "../types/types.js"; import { wrapHook } from "./internal/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; -import type { - FetchMoreFunction, - RefetchFunction, - SubscribeToMoreFunction, -} 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 { SkipToken } from "./constants.js"; @@ -293,7 +290,9 @@ function useBackgroundQuery_< { fetchMore, refetch, - subscribeToMore: queryRef.observable.subscribeToMore, + // TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here + subscribeToMore: queryRef.observable + .subscribeToMore as SubscribeToMoreFunction, }, ]; } diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index b9aa70d11e2..d520b96668c 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -6,6 +6,10 @@ import type { TypedDocumentNode, WatchQueryOptions, } from "../../core/index.js"; +import type { + SubscribeToMoreFunction, + SubscribeToMoreOptions, +} from "../../core/watchQueryOptions.js"; import { useApolloClient } from "./useApolloClient.js"; import { assertWrappedQueryRef, @@ -18,11 +22,7 @@ import type { CacheKey, QueryRef } from "../internal/index.js"; import type { LoadableQueryHookOptions } from "../types/types.js"; import { __use, useRenderGuard } from "./internal/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; -import type { - FetchMoreFunction, - RefetchFunction, - SubscribeToMoreFunction, -} from "./useSuspenseQuery.js"; +import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; import type { DeepPartial, @@ -269,7 +269,10 @@ export function useLoadableQuery< "The query has not been loaded. Please load the query." ); - return internalQueryRef.observable.subscribeToMore(options); + return internalQueryRef.observable.subscribeToMore( + // TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here + options as any as SubscribeToMoreOptions + ); }, [internalQueryRef] ); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index b608c4b8b6b..764c94cb377 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -289,9 +289,10 @@ export function useQueryInternals< watchQueryOptions ); - const obsQueryFields = React.useMemo< - Omit, "variables"> - >(() => bindObservableMethods(observable), [observable]); + const obsQueryFields = React.useMemo( + () => bindObservableMethods(observable), + [observable] + ); useRegisterSSRObservable(observable, renderPromises, ssrAllowed); @@ -825,7 +826,7 @@ const skipStandbyResult = maybeDeepFreeze({ function bindObservableMethods( observable: ObservableQuery -) { +): Omit, "variables"> { return { refetch: observable.refetch.bind(observable), reobserve: observable.reobserve.bind(observable), diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts index 8204e3e5df7..23c491c2d25 100644 --- a/src/react/hooks/useQueryRefHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -8,11 +8,8 @@ import { } from "../internal/index.js"; import type { QueryRef } from "../internal/index.js"; import type { OperationVariables } from "../../core/types.js"; -import type { - RefetchFunction, - FetchMoreFunction, - SubscribeToMoreFunction, -} from "./useSuspenseQuery.js"; +import type { SubscribeToMoreFunction } from "../../core/watchQueryOptions.js"; +import type { RefetchFunction, FetchMoreFunction } from "./useSuspenseQuery.js"; import type { FetchMoreQueryOptions } from "../../core/watchQueryOptions.js"; import { useApolloClient } from "./useApolloClient.js"; import { wrapHook } from "./internal/index.js"; @@ -121,6 +118,8 @@ function useQueryRefHandlers_< return { refetch, fetchMore, - subscribeToMore: internalQueryRef.observable.subscribeToMore, + // TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here + subscribeToMore: internalQueryRef.observable + .subscribeToMore as SubscribeToMoreFunction, }; } diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index faa0f44df6c..af634110fe6 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -11,6 +11,7 @@ import type { WatchQueryOptions, } from "../../core/index.js"; import { ApolloError, NetworkStatus } from "../../core/index.js"; +import type { SubscribeToMoreFunction } from "../../core/watchQueryOptions.js"; import type { DeepPartial } from "../../utilities/index.js"; import { isNonEmptyArray } from "../../utilities/index.js"; import { useApolloClient } from "./useApolloClient.js"; @@ -58,11 +59,6 @@ export type RefetchFunction< TVariables extends OperationVariables, > = ObservableQueryFields["refetch"]; -export type SubscribeToMoreFunction< - TData, - TVariables extends OperationVariables, -> = ObservableQueryFields["subscribeToMore"]; - export function useSuspenseQuery< TData, TVariables extends OperationVariables, @@ -277,7 +273,9 @@ function useSuspenseQuery_< [queryRef] ); - const subscribeToMore = queryRef.observable.subscribeToMore; + // TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here + const subscribeToMore = queryRef.observable + .subscribeToMore as SubscribeToMoreFunction; return React.useMemo< UseSuspenseQueryResult diff --git a/src/react/types/types.ts b/src/react/types/types.ts index f3ce870c714..7151fcbb688 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -19,7 +19,6 @@ import type { InternalRefetchQueriesInclude, WatchQueryOptions, WatchQueryFetchPolicy, - SubscribeToMoreOptions, ApolloQueryResult, FetchMoreQueryOptions, ErrorPolicy, @@ -28,6 +27,8 @@ import type { import type { MutationSharedOptions, SharedWatchQueryOptions, + SubscribeToMoreFunction, + UpdateQueryMapFn, } from "../../core/watchQueryOptions.js"; import type { MaybeMasked, Unmasked } from "../../masking/index.js"; @@ -90,23 +91,9 @@ export interface ObservableQueryFields< /** {@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; + subscribeToMore: SubscribeToMoreFunction; /** {@inheritDoc @apollo/client!QueryResultDocumentation#updateQuery:member} */ - updateQuery: ( - mapFn: ( - previousQueryResult: Unmasked, - options: Pick, "variables"> - ) => Unmasked - ) => void; + updateQuery: (mapFn: UpdateQueryMapFn) => void; /** {@inheritDoc @apollo/client!QueryResultDocumentation#refetch:member} */ refetch: ( variables?: Partial From 67c16c93897e36be980ba2139ee8bd3f24ab8558 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 6 Feb 2025 17:45:32 +0100 Subject: [PATCH 16/68] keep deferred inFlightLinkObservables until the response is finished (#12338) --- .changeset/quiet-apricots-reply.md | 6 + .size-limits.json | 4 +- src/config/jest/setup.ts | 3 + src/core/QueryManager.ts | 8 +- .../__tests__/ApolloClient/general.test.ts | 135 +++++++++++++++++- 5 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 .changeset/quiet-apricots-reply.md diff --git a/.changeset/quiet-apricots-reply.md b/.changeset/quiet-apricots-reply.md new file mode 100644 index 00000000000..2d52fd9f0d0 --- /dev/null +++ b/.changeset/quiet-apricots-reply.md @@ -0,0 +1,6 @@ +--- +"@apollo/client": patch +--- + +In case of a multipart response (e.g. with `@defer`), query deduplication will +now keep going until the final chunk has been received. diff --git a/.size-limits.json b/.size-limits.json index c7ac8973041..cb849c7a6cb 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42196, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34405 + "dist/apollo-client.min.cjs": 42225, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34432 } diff --git a/src/config/jest/setup.ts b/src/config/jest/setup.ts index 141d0e4132d..4c1ed3e5581 100644 --- a/src/config/jest/setup.ts +++ b/src/config/jest/setup.ts @@ -36,3 +36,6 @@ if (!Symbol.asyncDispose) { // @ts-ignore expect.addEqualityTesters([areApolloErrorsEqual, areGraphQLErrorsEqual]); + +// not available in JSDOM 🙄 +global.structuredClone = (val) => JSON.parse(JSON.stringify(val)); diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 066dc137de9..5a32bad1cca 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -1182,8 +1182,12 @@ export class QueryManager { ]); observable = entry.observable = concast; - concast.beforeNext(() => { - inFlightLinkObservables.remove(printedServerQuery, varJson); + concast.beforeNext(function cb(method, arg: FetchResult) { + if (method === "next" && "hasNext" in arg && arg.hasNext) { + concast.beforeNext(cb); + } else { + inFlightLinkObservables.remove(printedServerQuery, varJson); + } }); } } else { diff --git a/src/core/__tests__/ApolloClient/general.test.ts b/src/core/__tests__/ApolloClient/general.test.ts index e18dab64fd7..a11cdde119b 100644 --- a/src/core/__tests__/ApolloClient/general.test.ts +++ b/src/core/__tests__/ApolloClient/general.test.ts @@ -9,7 +9,11 @@ import { Observable, Observer, } from "../../../utilities/observables/Observable"; -import { ApolloLink, FetchResult } from "../../../link/core"; +import { + ApolloLink, + FetchResult, + type RequestHandler, +} from "../../../link/core"; import { InMemoryCache } from "../../../cache"; // mocks @@ -31,7 +35,11 @@ import { wait } from "../../../testing/core"; import { ApolloClient } from "../../../core"; import { mockFetchQuery } from "../ObservableQuery"; import { Concast, print } from "../../../utilities"; -import { ObservableStream, spyOnConsole } from "../../../testing/internal"; +import { + mockDeferStream, + ObservableStream, + spyOnConsole, +} from "../../../testing/internal"; describe("ApolloClient", () => { const getObservableStream = ({ @@ -6522,6 +6530,129 @@ describe("ApolloClient", () => { ) ).toBeUndefined(); }); + + it("deduplicates queries as long as a query still has deferred chunks", async () => { + const query = gql` + query LazyLoadLuke { + people(id: 1) { + id + name + friends { + id + ... @defer { + name + } + } + } + } + `; + + const outgoingRequestSpy = jest.fn(((operation, forward) => + forward(operation)) satisfies RequestHandler); + const defer = mockDeferStream(); + const client = new ApolloClient({ + cache: new InMemoryCache({}), + link: new ApolloLink(outgoingRequestSpy).concat(defer.httpLink), + }); + + const query1 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + const query2 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const initialData = { + people: { + __typename: "Person", + id: 1, + name: "Luke", + friends: [ + { + __typename: "Person", + id: 5, + } as { __typename: "Person"; id: number; name?: string }, + { + __typename: "Person", + id: 8, + } as { __typename: "Person"; id: number; name?: string }, + ], + }, + }; + const initialResult = { + data: initialData, + loading: false, + networkStatus: 7, + }; + + defer.enqueueInitialChunk({ + data: initialData, + hasNext: true, + }); + + await expect(query1).toEmitFetchResult(initialResult); + await expect(query2).toEmitFetchResult(initialResult); + + const query3 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + await expect(query3).toEmitFetchResult(initialResult); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const firstChunk = { + incremental: [ + { + data: { + name: "Leia", + }, + path: ["people", "friends", 0], + }, + ], + hasNext: true, + }; + const resultAfterFirstChunk = structuredClone(initialResult); + resultAfterFirstChunk.data.people.friends[0].name = "Leia"; + + defer.enqueueSubsequentChunk(firstChunk); + + await expect(query1).toEmitFetchResult(resultAfterFirstChunk); + await expect(query2).toEmitFetchResult(resultAfterFirstChunk); + await expect(query3).toEmitFetchResult(resultAfterFirstChunk); + + const query4 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + expect(query4).toEmitFetchResult(resultAfterFirstChunk); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const secondChunk = { + incremental: [ + { + data: { + name: "Han Solo", + }, + path: ["people", "friends", 1], + }, + ], + hasNext: false, + }; + const resultAfterSecondChunk = structuredClone(resultAfterFirstChunk); + resultAfterSecondChunk.data.people.friends[1].name = "Han Solo"; + + defer.enqueueSubsequentChunk(secondChunk); + + await expect(query1).toEmitFetchResult(resultAfterSecondChunk); + await expect(query2).toEmitFetchResult(resultAfterSecondChunk); + await expect(query3).toEmitFetchResult(resultAfterSecondChunk); + await expect(query4).toEmitFetchResult(resultAfterSecondChunk); + + const query5 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + expect(query5).not.toEmitAnything(); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(2); + }); }); describe("missing cache field warnings", () => { From 0a0f80123912dbff8edf779270b6216ca1ff9845 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Feb 2025 11:44:36 -0700 Subject: [PATCH 17/68] Version Packages (#12343) Co-authored-by: github-actions[bot] --- .changeset/large-timers-lay.md | 5 ----- .changeset/lazy-clocks-sip.md | 5 ----- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 .changeset/large-timers-lay.md delete mode 100644 .changeset/lazy-clocks-sip.md diff --git a/.changeset/large-timers-lay.md b/.changeset/large-timers-lay.md deleted file mode 100644 index 8712178dbf1..00000000000 --- a/.changeset/large-timers-lay.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -`useReadQuery`/`useQueryRefHandlers`: Fix a "hook order" warning that might be emitted in React 19 dev mode. diff --git a/.changeset/lazy-clocks-sip.md b/.changeset/lazy-clocks-sip.md deleted file mode 100644 index 80810e8cb87..00000000000 --- a/.changeset/lazy-clocks-sip.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Add `graphql-ws` `^6.0.3` as a valid `peerDependency` diff --git a/CHANGELOG.md b/CHANGELOG.md index de09aed465e..939ebd5b04e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # @apollo/client +## 3.12.10 + +### Patch Changes + +- [#12341](https://github.com/apollographql/apollo-client/pull/12341) [`f2bb0b9`](https://github.com/apollographql/apollo-client/commit/f2bb0b9955564e432345ee8bd431290e698d092c) Thanks [@phryneas](https://github.com/phryneas)! - `useReadQuery`/`useQueryRefHandlers`: Fix a "hook order" warning that might be emitted in React 19 dev mode. + +- [#12342](https://github.com/apollographql/apollo-client/pull/12342) [`219b26b`](https://github.com/apollographql/apollo-client/commit/219b26ba5a697981ad700e05b926d42db0fb8e59) Thanks [@phryneas](https://github.com/phryneas)! - Add `graphql-ws` `^6.0.3` as a valid `peerDependency` + ## 3.12.9 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 41da0d0073f..2358a8dee88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.12.9", + "version": "3.12.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.12.9", + "version": "3.12.10", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index f082c732038..4b8d2c5657f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.12.9", + "version": "3.12.10", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 60a3ba068dc55c79aaa20d0cd1822c1c676a3c20 Mon Sep 17 00:00:00 2001 From: Matt Peake <7741049+peakematt@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:58:17 -0500 Subject: [PATCH 18/68] remove cci-based security scans (#12339) --- .circleci/config.yml | 17 ----------------- .size-limits.json | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 0b3aee29c12..57f5c735e67 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,8 +1,5 @@ version: 2.1 -orbs: - secops: apollo/circleci-secops-orb@2.0.7 - jobs: # Filesize: # docker: @@ -183,17 +180,3 @@ workflows: - "@types/react@18 @types/react-dom@18" - "@types/react@19 @types/react-dom@19" - "typescript@next" - security-scans: - jobs: - - secops/gitleaks: - context: - - platform-docker-ro - - github-orb - - secops-oidc - git-base-revision: <<#pipeline.git.base_revision>><><> - git-revision: << pipeline.git.revision >> - - secops/semgrep: - context: - - secops-oidc - - github-orb - git-base-revision: <<#pipeline.git.base_revision>><><> diff --git a/.size-limits.json b/.size-limits.json index f7f8f2606dc..ff50cf157f5 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { "dist/apollo-client.min.cjs": 41642, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34384 + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34385 } From 716d02ec9c5b1448f50cb50a0306a345310a2342 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 7 Feb 2025 17:47:00 +0100 Subject: [PATCH 19/68] deprecate `use(Lazy)Query` `on(Error|Completed)` callbacks (#12340) * deprecate `use(Lazy)Query` `on(Error|Completed)` callbacks * changeset * Apply suggestions from code review Co-authored-by: Jerel Miller * Update src/react/types/deprecation.md Co-authored-by: Jerel Miller * Apply suggestions from code review * Update src/react/types/deprecation.md * Update src/react/types/deprecation.md * Slight tweaks to changes around behavior section * Slight tweak to defer section * Add section on our recommendation * Update section for bugs * Minor tweak to solutions section * Mention switch in behavior * Slight tweak to end * remove draft file, add links to issue * api-extractor --------- Co-authored-by: Jerel Miller --- .api-reports/api-report-react.api.md | 4 +++ .../api-report-react_components.api.md | 2 ++ .api-reports/api-report-react_context.api.md | 2 ++ .api-reports/api-report-react_hooks.api.md | 4 +++ .api-reports/api-report-react_internal.api.md | 2 ++ .api-reports/api-report-react_ssr.api.md | 2 ++ .api-reports/api-report.api.md | 4 +++ .changeset/heavy-pumas-boil.md | 6 ++++ src/react/types/types.ts | 28 ++++++++++++++++--- 9 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 .changeset/heavy-pumas-boil.md diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 09cf76aaa3c..09912587c02 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1048,7 +1048,9 @@ export interface LazyQueryHookExecOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; } @@ -1596,7 +1598,9 @@ export interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index 314d50b5721..50d76be94a8 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -1419,7 +1419,9 @@ export interface QueryComponentOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index 375cbf7b818..bd4c1aeb0b3 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -1346,7 +1346,9 @@ interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index f855c73cb04..d4a08afa51f 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -997,7 +997,9 @@ interface LazyQueryHookExecOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; } @@ -1469,7 +1471,9 @@ const QUERY_REF_BRAND: unique symbol; interface QueryFunctionOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 9bb666cb03a..e44efaeb697 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -1566,7 +1566,9 @@ const QUERY_REFERENCE_SYMBOL: unique symbol; interface QueryFunctionOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index 387a028e627..e02d9d2905d 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1331,7 +1331,9 @@ interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 64fddeb0377..cd50f3fc69a 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -1465,7 +1465,9 @@ export interface LazyQueryHookExecOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; } @@ -2180,7 +2182,9 @@ export interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } diff --git a/.changeset/heavy-pumas-boil.md b/.changeset/heavy-pumas-boil.md new file mode 100644 index 00000000000..8c523755db0 --- /dev/null +++ b/.changeset/heavy-pumas-boil.md @@ -0,0 +1,6 @@ +--- +"@apollo/client": minor +--- + +Deprecate the `onCompleted` and `onError` callbacks of `useQuery` and `useLazyQuery`. +For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 7151fcbb688..42f90cec7de 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -68,9 +68,19 @@ export interface QueryFunctionOptions< > extends BaseQueryOptions { /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#skip:member} */ skip?: boolean; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} */ + /** + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} + * + * @deprecated This option will be removed in the next major version of Apollo Client. + * For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + */ onCompleted?: (data: MaybeMasked) => void; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} */ + /** + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} + * + * @deprecated This option will be removed in the next major version of Apollo Client. + * For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + */ onError?: (error: ApolloError) => void; // Default WatchQueryOptions for this useQuery, providing initial values for @@ -167,9 +177,19 @@ export interface LazyQueryHookOptions< TData = any, TVariables extends OperationVariables = OperationVariables, > extends BaseQueryOptions { - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} */ + /** + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} + * + * @deprecated This option will be removed in the next major version of Apollo Client. + * For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + */ onCompleted?: (data: MaybeMasked) => void; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} */ + /** + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} + * + * @deprecated This option will be removed in the next major version of Apollo Client. + * For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + */ onError?: (error: ApolloError) => void; /** @internal */ From 1855684a5182913fddb9507453cc8fc82e4ffefe Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 7 Feb 2025 09:47:57 -0700 Subject: [PATCH 20/68] Remove addTypename setting in testing docs (#12353) --- docs/source/development-testing/testing.mdx | 34 ++++++++------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/docs/source/development-testing/testing.mdx b/docs/source/development-testing/testing.mdx index 3453f2559f4..c53ea71ca14 100644 --- a/docs/source/development-testing/testing.mdx +++ b/docs/source/development-testing/testing.mdx @@ -62,7 +62,7 @@ const mocks = []; // We'll fill this in next it("renders without error", async () => { render( - + ); @@ -87,7 +87,7 @@ const mocks = [ }, result: { data: { - dog: { id: "1", name: "Buck", breed: "bulldog" } + dog: { __typename: "Dog", id: "1", name: "Buck", breed: "bulldog" } } } } @@ -106,7 +106,7 @@ result: (variables) => { // `variables` is optional return { data: { - dog: { id: '1', name: 'Buck', breed: 'bulldog' }, + dog: { __typename: 'Dog', id: '1', name: 'Buck', breed: 'bulldog' }, }, } }, @@ -132,7 +132,7 @@ const mocks = [ }, result: { data: { - dog: { id: "1", name: "Buck", breed: "bulldog" } + dog: { __typename: "Dog", id: "1", name: "Buck", breed: "bulldog" } } } } @@ -140,7 +140,7 @@ const mocks = [ it("renders without error", async () => { render( - + ); @@ -169,7 +169,7 @@ const mocks = [ }, result: { data: { - dog: { id: "1", name: "Buck", breed: "bulldog" } + dog: { __typename: "Dog", id: "1", name: "Buck", breed: "bulldog" } } }, maxUsageCount: 2, // The mock can be used twice before it's removed, default is 1 @@ -196,7 +196,7 @@ const dogMock: MockedResponse = { }, variableMatcher: (variables) => true, result: { - data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } }, + data: { dog: { __typename: 'Dog', id: 1, name: 'Buck', breed: 'poodle' } }, }, }; ``` @@ -212,7 +212,7 @@ const dogMock: MockedResponse = { }, variableMatcher: jest.fn().mockReturnValue(true), result: { - data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } }, + data: { dog: { __typename: 'Dog', id: 1, name: 'Buck', breed: 'poodle' } }, }, }; @@ -221,14 +221,6 @@ expect(variableMatcher).toHaveBeenCalledWith(expect.objectContaining({ })); ``` -### 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). - -We _don't_ want to automatically add `__typename` to `GET_DOG_QUERY` in our test, because then it won't match the shape of the query that our mock is expecting. - -Unless you explicitly configure your mocks to expect a `__typename` field, always set `addTypename` to `false` in your tests. - ## Testing the "loading" and "success" states To test how your component is rendered after its query completes, Testing Library provides several `findBy` methods. From the [Testing Library docs](https://testing-library.com/docs/dom-testing-library/api-async/#findby-queries): @@ -248,11 +240,11 @@ it("should render dog", async () => { variables: { name: "Buck" } }, result: { - data: { dog: { id: 1, name: "Buck", breed: "poodle" } } + data: { dog: { __typename: "Dog", id: 1, name: "Buck", breed: "poodle" } } } }; render( - + ); @@ -282,7 +274,7 @@ it("should show error UI", async () => { error: new Error("An error occurred") }; render( - + ); @@ -368,7 +360,7 @@ The following test _does_ execute the mutation by clicking the button: ```jsx title="delete-dog.test.js" it("should render loading and success states on delete", async () => { - const deleteDog = { name: "Buck", breed: "Poodle", id: 1 }; + const deleteDog = { __typename: "Dog", name: "Buck", breed: "Poodle", id: 1 }; const mocks = [ { request: { @@ -380,7 +372,7 @@ it("should render loading and success states on delete", async () => { ]; render( - + ); From a24ef9474f8f7a864f8b866563f8f7e661d2533f Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 7 Feb 2025 18:10:15 +0100 Subject: [PATCH 21/68] fix missing main.d.cts (#12354) --- .changeset/spotty-poets-walk.md | 5 +++++ config/prepareDist.js | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/spotty-poets-walk.md diff --git a/.changeset/spotty-poets-walk.md b/.changeset/spotty-poets-walk.md new file mode 100644 index 00000000000..aa9f7eef81a --- /dev/null +++ b/.changeset/spotty-poets-walk.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix missing `main.d.cts` file diff --git a/config/prepareDist.js b/config/prepareDist.js index 68bfbce2c2e..e96c108840c 100644 --- a/config/prepareDist.js +++ b/config/prepareDist.js @@ -97,7 +97,6 @@ entryPoints.forEach(function buildCts({ dirs, bundleName = dirs[dirs.length - 1], }) { - if (!dirs.length) return; fs.writeFileSync( path.join(distRoot, ...dirs, `${bundleName}.d.cts`), 'export * from "./index.d.ts";\n' From 3da908b1dde73847805a41c287a83700b2b88887 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 7 Feb 2025 10:16:04 -0700 Subject: [PATCH 22/68] Fix emitted `fetchMore` result from `observableQuery` for no-cache queries (#12351) --- .changeset/curvy-ads-matter.md | 5 ++ .size-limits.json | 4 +- src/__tests__/fetchMore.ts | 139 ++++++++++++++++++++++++++++++++- src/core/ObservableQuery.ts | 7 +- 4 files changed, 151 insertions(+), 4 deletions(-) create mode 100644 .changeset/curvy-ads-matter.md diff --git a/.changeset/curvy-ads-matter.md b/.changeset/curvy-ads-matter.md new file mode 100644 index 00000000000..81a7214a06b --- /dev/null +++ b/.changeset/curvy-ads-matter.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fixes an issue where the wrong `networkStatus` and `loading` value was emitted from `observableQuery` when calling `fetchMore` with a `no-cache` fetch policy. The `networkStatus` now properly reports as `ready` and `loading` as `false` after the result is returned. diff --git a/.size-limits.json b/.size-limits.json index ff50cf157f5..6f950d7282a 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41642, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34385 + "dist/apollo-client.min.cjs": 41649, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34394 } diff --git a/src/__tests__/fetchMore.ts b/src/__tests__/fetchMore.ts index 9d53d363789..56f3c50a377 100644 --- a/src/__tests__/fetchMore.ts +++ b/src/__tests__/fetchMore.ts @@ -22,7 +22,7 @@ import { } from "../cache"; import { MockedResponse, mockSingleLink } from "../testing"; -import { ObservableStream } from "../testing/internal"; +import { ObservableStream, setupPaginatedCase } from "../testing/internal"; describe("updateQuery on a simple query", () => { const query = gql` @@ -1789,3 +1789,140 @@ describe("fetchMore on an observable query with connection", () => { }); }); }); + +test("uses updateQuery to update the result of the query with no-cache queries", async () => { + const { query, link } = setupPaginatedCase(); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + const observable = client.watchQuery({ + query, + fetchPolicy: "no-cache", + notifyOnNetworkStatusChange: true, + variables: { limit: 2 }, + }); + + const stream = new ObservableStream(observable); + + await expect(stream).toEmitApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + let fetchMoreResult = await observable.fetchMore({ + variables: { offset: 2 }, + updateQuery: (prev, { fetchMoreResult }) => ({ + letters: prev.letters.concat(fetchMoreResult.letters), + }), + }); + + expect(fetchMoreResult).toEqualApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + ], + }, + loading: true, + networkStatus: NetworkStatus.fetchMore, + }); + + await expect(stream).toEmitApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + // Ensure we store the merged result as the last result + expect(observable.getCurrentResult(false)).toEqualApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); + + fetchMoreResult = await observable.fetchMore({ + variables: { offset: 4 }, + updateQuery: (_, { fetchMoreResult }) => fetchMoreResult, + }); + + expect(fetchMoreResult).toEqualApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "E", position: 5 }, + { __typename: "Letter", letter: "F", position: 6 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + loading: true, + networkStatus: NetworkStatus.fetchMore, + }); + + await expect(stream).toEmitApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "E", position: 5 }, + { __typename: "Letter", letter: "F", position: 6 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + expect(observable.getCurrentResult(false)).toEqualApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "E", position: 5 }, + { __typename: "Letter", letter: "F", position: 6 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); +}); diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 2a70e6e9097..4397c518518 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -585,7 +585,12 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, }); this.reportResult( - { ...lastResult, data: data as TData }, + { + ...lastResult, + networkStatus: originalNetworkStatus!, + loading: isNetworkRequestInFlight(originalNetworkStatus), + data: data as TData, + }, this.variables ); } From d79aec054ef8632e03d61024f529a27692d0d55a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:22:55 -0700 Subject: [PATCH 23/68] Version Packages (#12355) Co-authored-by: github-actions[bot] --- .changeset/curvy-ads-matter.md | 5 ----- .changeset/spotty-poets-walk.md | 5 ----- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 .changeset/curvy-ads-matter.md delete mode 100644 .changeset/spotty-poets-walk.md diff --git a/.changeset/curvy-ads-matter.md b/.changeset/curvy-ads-matter.md deleted file mode 100644 index 81a7214a06b..00000000000 --- a/.changeset/curvy-ads-matter.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fixes an issue where the wrong `networkStatus` and `loading` value was emitted from `observableQuery` when calling `fetchMore` with a `no-cache` fetch policy. The `networkStatus` now properly reports as `ready` and `loading` as `false` after the result is returned. diff --git a/.changeset/spotty-poets-walk.md b/.changeset/spotty-poets-walk.md deleted file mode 100644 index aa9f7eef81a..00000000000 --- a/.changeset/spotty-poets-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fix missing `main.d.cts` file diff --git a/CHANGELOG.md b/CHANGELOG.md index 939ebd5b04e..090568c7df4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # @apollo/client +## 3.12.11 + +### Patch Changes + +- [#12351](https://github.com/apollographql/apollo-client/pull/12351) [`3da908b`](https://github.com/apollographql/apollo-client/commit/3da908b1dde73847805a41c287a83700b2b88887) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fixes an issue where the wrong `networkStatus` and `loading` value was emitted from `observableQuery` when calling `fetchMore` with a `no-cache` fetch policy. The `networkStatus` now properly reports as `ready` and `loading` as `false` after the result is returned. + +- [#12354](https://github.com/apollographql/apollo-client/pull/12354) [`a24ef94`](https://github.com/apollographql/apollo-client/commit/a24ef9474f8f7a864f8b866563f8f7e661d2533f) Thanks [@phryneas](https://github.com/phryneas)! - Fix missing `main.d.cts` file + ## 3.12.10 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 2358a8dee88..12913c788ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.12.10", + "version": "3.12.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.12.10", + "version": "3.12.11", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4b8d2c5657f..73841878cf3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.12.10", + "version": "3.12.11", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From e4c3dc441a42182f3a6d39f142f369b8b08bdab2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 7 Feb 2025 10:32:52 -0700 Subject: [PATCH 24/68] Update size limits --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index cb849c7a6cb..5b1f9c2bf72 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42225, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34432 + "dist/apollo-client.min.cjs": 42232, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34444 } From 88378b25b81163964567694fdccfb0dc34433bac Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 17:33:54 +0000 Subject: [PATCH 25/68] Prepare for rc release --- .changeset/pre.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 54334695736..ba55303eb3c 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,6 +1,6 @@ { "mode": "pre", - "tag": "beta", + "tag": "rc", "initialVersions": { "@apollo/client": "3.12.2" }, From 924030b74b4dba5675b0288f426c04cbcb618172 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Feb 2025 10:50:44 -0700 Subject: [PATCH 26/68] Version Packages (rc) (#12337) --- .changeset/pre.json | 14 ++++++++-- .size-limits.json | 4 +-- CHANGELOG.md | 64 +++++++++++++++++++++++++++++++++++++++++++++ package-lock.json | 4 +-- package.json | 2 +- 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index ba55303eb3c..20a35fef3f9 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -4,5 +4,15 @@ "initialVersions": { "@apollo/client": "3.12.2" }, - "changesets": [] -} \ No newline at end of file + "changesets": [ + "blue-comics-train", + "bright-guests-chew", + "fluffy-worms-fail", + "heavy-pumas-boil", + "khaki-cars-develop", + "pretty-planets-cough", + "quiet-apricots-reply", + "sharp-windows-switch", + "tough-years-destroy" + ] +} diff --git a/.size-limits.json b/.size-limits.json index 5b1f9c2bf72..89490c6022d 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42232, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34444 + "dist/apollo-client.min.cjs": 42235, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34448 } diff --git a/CHANGELOG.md b/CHANGELOG.md index 090568c7df4..db3af4247c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,69 @@ # @apollo/client +## 3.13.0-rc.0 + +### Minor Changes + +- [#12066](https://github.com/apollographql/apollo-client/pull/12066) [`c01da5d`](https://github.com/apollographql/apollo-client/commit/c01da5da639d4d9e882d380573b7876df4a1d65b) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds a new `useSuspenseFragment` hook. + + `useSuspenseFragment` suspends until `data` is complete. It is a drop-in replacement for `useFragment` when you prefer to use Suspense to control the loading state of a fragment. + +- [#12174](https://github.com/apollographql/apollo-client/pull/12174) [`ba5cc33`](https://github.com/apollographql/apollo-client/commit/ba5cc330f8734a989eef71e883861f848388ac0c) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Ensure errors thrown in the `onCompleted` callback from `useMutation` don't call `onError`. + +- [#12340](https://github.com/apollographql/apollo-client/pull/12340) [`716d02e`](https://github.com/apollographql/apollo-client/commit/716d02ec9c5b1448f50cb50a0306a345310a2342) Thanks [@phryneas](https://github.com/phryneas)! - Deprecate the `onCompleted` and `onError` callbacks of `useQuery` and `useLazyQuery`. + For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Provide a more type-safe option for the previous data value passed to `observableQuery.updateQuery`. Using it could result in crashes at runtime as this callback could be called with partial data even though its type reported the value as a complete result. + + The `updateQuery` callback function is now called with a new type-safe `previousData` property and a new `complete` property in the 2nd argument that determines whether `previousData` is a complete or partial result. + + As a result of this change, it is recommended to use the `previousData` property passed to the 2nd argument of the callback rather than using the previous data value from the first argument since that value is not type-safe. The first argument is now deprecated and will be removed in a future version of Apollo Client. + + ```ts + observableQuery.updateQuery( + (unsafePreviousData, { previousData, complete }) => { + previousData; + // ^? TData | DeepPartial | undefined + + if (complete) { + previousData; + // ^? TData + } else { + previousData; + // ^? DeepPartial | undefined + } + } + ); + ``` + +- [#12174](https://github.com/apollographql/apollo-client/pull/12174) [`ba5cc33`](https://github.com/apollographql/apollo-client/commit/ba5cc330f8734a989eef71e883861f848388ac0c) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Reject the mutation promise if errors are thrown in the `onCompleted` callback of `useMutation`. + +### Patch Changes + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Fix the return type of the `updateQuery` function to allow for `undefined`. `updateQuery` had the ability to bail out of the update by returning a falsey value, but the return type enforced a query value. + + ```ts + observableQuery.updateQuery( + (unsafePreviousData, { previousData, complete }) => { + if (!complete) { + // Bail out of the update by returning early + return; + } + + // ... + } + ); + ``` + +- [#12296](https://github.com/apollographql/apollo-client/pull/12296) [`2422df2`](https://github.com/apollographql/apollo-client/commit/2422df202a7ec71365d5a8ab5b3b554fcf60e4af) Thanks [@Cellule](https://github.com/Cellule)! - Deprecate option `ignoreResults` in `useMutation`. + Once this option is removed, existing code still using it might see increase in re-renders. + If you don't want to synchronize your component state with the mutation, please use `useApolloClient` to get your ApolloClient instance and call `client.mutate` directly. + +- [#12338](https://github.com/apollographql/apollo-client/pull/12338) [`67c16c9`](https://github.com/apollographql/apollo-client/commit/67c16c93897e36be980ba2139ee8bd3f24ab8558) Thanks [@phryneas](https://github.com/phryneas)! - In case of a multipart response (e.g. with `@defer`), query deduplication will + now keep going until the final chunk has been received. + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Fix the type of the `variables` property passed as the 2nd argument to the `subscribeToMore` `updateQuery` callback. This was previously reported as the `variables` type for the subscription itself, but is now properly typed as the query `variables`. + ## 3.12.11 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 12913c788ed..413630b60cb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.12.11", + "version": "3.13.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.12.11", + "version": "3.13.0-rc.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 73841878cf3..efd9ef95ecc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.12.11", + "version": "3.13.0-rc.0", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 71affea39f071523f753fc79af3af66e97c4974a Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 6 Feb 2025 11:43:46 +0100 Subject: [PATCH 27/68] update `node-version` for React canary test cronjob --- .github/workflows/scheduled-test-canary.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scheduled-test-canary.yml b/.github/workflows/scheduled-test-canary.yml index 42d5442d948..f94dc74be0b 100644 --- a/.github/workflows/scheduled-test-canary.yml +++ b/.github/workflows/scheduled-test-canary.yml @@ -28,7 +28,7 @@ jobs: ref: ${{ matrix.branch }} - uses: actions/setup-node@v4 with: - node-version: 22.x + node-version: ">=23.6.0" - uses: bahmutov/npm-install@v1 - run: | npm install react@${{ matrix.tag }} react-dom@${{ matrix.tag }} From 27e1532b38b7381cf94b584f539d3d47fdce79d5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 11 Feb 2025 10:01:57 -0700 Subject: [PATCH 28/68] Add documentation for `useSuspenseFragment` (#12356) Co-authored-by: Maria Elisabeth Schreiber --- docs/source/data/fragments.mdx | 69 +++++++++++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/docs/source/data/fragments.mdx b/docs/source/data/fragments.mdx index d8f4fdf4373..4124d0eb53a 100644 --- a/docs/source/data/fragments.mdx +++ b/docs/source/data/fragments.mdx @@ -561,7 +561,7 @@ function Item(props: { item: { __typename: 'Item', id: number }}) { const { complete, data } = useFragment({ fragment: ItemFragment, fragmentName: "ItemFragment", - from: item + from: props.item }); return
  • {complete ? data.text : "incomplete"}
  • ; @@ -573,7 +573,7 @@ function Item(props) { const { complete, data } = useFragment({ fragment: ITEM_FRAGMENT, fragmentName: "ItemFragment", - from: item + from: props.item }); return
  • {complete ? data.text : "incomplete"}
  • ; @@ -589,6 +589,71 @@ function Item(props) { See the [API reference](../api/react/hooks#usefragment) for more details on the supported options. + +## `useSuspenseFragment` + + +For those that have integrated with React [Suspense](https://react.dev/reference/react/Suspense), `useSuspenseFragment` is available as a drop-in replacement for `useFragment`. `useSuspenseFragment` works identically to `useFragment` but will suspend while `data` is incomplete. + +Let's update the example from the previous section to use `useSuspenseFragment`. First, we'll update our `Item` component and replace `useFragment` with `useSuspenseFragment`. Since we are using Suspense, we no longer have to check for a `complete` property to determine if the result is complete because the component will suspend otherwise. + +```tsx +import { useSuspenseFragment } from "@apollo/client"; + +function Item(props) { + const { data } = useSuspenseFragment({ + fragment: ITEM_FRAGMENT, + fragmentName: "ItemFragment", + from: props.item + }); + + return
  • {data.text}
  • ; +} +``` + +Next, we'll will wrap our `Item` components in a `Suspense` boundary to show a loading indicator if the data from `ItemFragment` is not complete. Since we're using Suspense, we'll replace `useQuery` with `useSuspenseQuery` as well: + +```tsx +function List() { + const { data } = useSuspenseQuery(listQuery); + + return ( +
      + {data.list.map(item => ( + }> + + + ))} +
    + ); +} +``` + +And that's it! Suspense made our `Item` component a bit more succinct since we no longer need to check the `complete` property to determine if we can safely use `data`. + + +In most cases, `useSuspenseFragment` will not suspend when rendered as a child of a query component. In this example `useSuspenseQuery` loads the full query data before each `Item` is rendered so the `data` inside each fragment is already complete. The `Suspense` boundary in this example ensures that a loading spinner is shown if field data is removed for any given item in the list in the cache, such as when a manual cache update is performed. + + +### Using `useSuspenseFragment` with `@defer` + +`useSuspenseFragment` is helpful when combined with the [`@defer` directive](./directives#defer) to show a loading state while the fragment data is streamed to the query. Let's update our `GetItemList` query to defer loading the `ItemFragment`'s fields. + +```graphql +query GetItemList { + list { + id + ...ItemFragment @defer + } +} +``` + +Our list will now render as soon as our list returns but before the data for `ItemFragment` is loaded. + + +You **must** ensure that any key fields used to identify the object passed to the `from` option are not deferred. If they are, you risk suspending the `useSuspenseFragment` hook forever. If you need to defer loading key fields, conditionally render the component until the object passed to the `from` option is identifiable by the cache. + + ## Data masking From 66dc5b98bfdd24e4433823242333c1b31c6780c7 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 12 Feb 2025 09:24:24 -0700 Subject: [PATCH 29/68] Update ROADMAP.md --- ROADMAP.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index f8dac57c1b7..84c080c3bf0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 🔮 Apollo Client Ecosystem Roadmap -**Last updated: 2025-02-05** +**Last updated: 2025-02-12** For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md). @@ -24,11 +24,11 @@ _Release candidate - February 6th_ - `onCompleted` and `onError` callback deprecations #### [4.0.0](https://github.com/apollographql/apollo-client/milestone/31) - TBD -_Release candidate - TBD_ +_Release candidate - Mid April 2025_ -#### Upcoming features +#### Upcoming changes -##### 3.x.x +##### 3.14.0 - Deprecations and preparations for 4.0 ### GraphQL Testing Library @@ -58,3 +58,4 @@ _These changes will take longer than anticipated due to prioritization on Apollo - Support for `@defer` with `PreloadQuery` (merged) - Support for Apollo Client Streaming in TanStack Router - Support for Apollo Client Streaming in React Router 7 (merged) +- Remove `experimental` label from Next.js integration From d71f2e8392e80d2bdcd237b4667e8c624a66ee0a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 13 Feb 2025 09:50:29 -0700 Subject: [PATCH 30/68] Require `variables` option in `useSuspenseFragment` when there are required variables (#12363) --- .api-reports/api-report-react.api.md | 18 +- .api-reports/api-report-react_hooks.api.md | 25 ++- .api-reports/api-report-react_internal.api.md | 18 +- .api-reports/api-report.api.md | 18 +- .size-limits.json | 2 +- .../__tests__/useSuspenseFragment.test.tsx | 157 ++++++++++++++++++ src/react/hooks/useSuspenseFragment.ts | 42 +++-- .../query-preloader/createQueryPreloader.ts | 18 +- src/react/types/types.ts | 17 ++ 9 files changed, 246 insertions(+), 69 deletions(-) diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 09912587c02..380070343f9 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1547,8 +1547,6 @@ export interface PreloadQueryFunction { (query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg>): PreloadedQueryRef; } -// 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; @@ -2473,13 +2471,13 @@ export function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; // @public (undocumented) -interface UseSuspenseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { - client?: ApolloClient; - // (undocumented) +type UseSuspenseFragmentOptions = { + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; from: From; - // (undocumented) optimistic?: boolean; -} + client?: ApolloClient; +} & VariablesOption>; // @public (undocumented) export type UseSuspenseFragmentResult = { @@ -2546,11 +2544,11 @@ export interface UseSuspenseQueryResult = [ +export type VariablesOption = [ TVariables ] extends [never] ? { variables?: Record; -} : {} extends OnlyRequiredProperties ? { +} : Record extends OnlyRequiredProperties ? { variables?: TVariables; } : { variables: TVariables; @@ -2603,7 +2601,7 @@ interface WatchQueryOptions(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; +// Warning: (ae-forgotten-export) The symbol "VariablesOption" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface UseSuspenseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { - client?: ApolloClient; - // (undocumented) +type UseSuspenseFragmentOptions = { + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; from: From; - // (undocumented) optimistic?: boolean; -} + client?: ApolloClient; +} & VariablesOption>; // @public (undocumented) export type UseSuspenseFragmentResult = { @@ -2380,6 +2382,17 @@ export interface UseSuspenseQueryResult; } +// @public (undocumented) +type VariablesOption = [ +TVariables +] extends [never] ? { + variables?: Record; +} : Record extends OnlyRequiredProperties ? { + variables?: TVariables; +} : { + variables: TVariables; +}; + // @public interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; @@ -2427,7 +2440,7 @@ interface WatchQueryOptions(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; // @public (undocumented) -interface UseSuspenseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { - client?: ApolloClient; - // (undocumented) +type UseSuspenseFragmentOptions = { + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; from: From; - // (undocumented) optimistic?: boolean; -} + client?: ApolloClient; +} & VariablesOption>; // @public (undocumented) type UseSuspenseFragmentResult = { @@ -2448,7 +2448,7 @@ type VariablesOption = [ TVariables ] extends [never] ? { variables?: Record; -} : {} extends OnlyRequiredProperties ? { +} : Record extends OnlyRequiredProperties ? { variables?: TVariables; } : { variables: TVariables; @@ -2549,9 +2549,9 @@ export function wrapQueryRef(inter // src/core/watchQueryOptions.ts:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:51:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:75:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useSuspenseFragment.ts:60:5 - (ae-forgotten-export) The symbol "From" needs to be exported by the entry point index.d.ts -// src/react/query-preloader/createQueryPreloader.ts:145:3 - (ae-forgotten-export) The symbol "PreloadQueryFetchPolicy" needs to be exported by the entry point index.d.ts -// src/react/query-preloader/createQueryPreloader.ts:167:5 - (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts +// src/react/hooks/useSuspenseFragment.ts:70:5 - (ae-forgotten-export) The symbol "From" needs to be exported by the entry point index.d.ts +// src/react/query-preloader/createQueryPreloader.ts:77:5 - (ae-forgotten-export) The symbol "PreloadQueryFetchPolicy" needs to be exported by the entry point index.d.ts +// src/react/query-preloader/createQueryPreloader.ts:117:3 - (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index cd50f3fc69a..3c3a0f2d981 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2118,8 +2118,6 @@ export interface PreloadQueryFunction { (query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg>): PreloadedQueryRef; } -// 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; @@ -3134,13 +3132,13 @@ export function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; // @public (undocumented) -interface UseSuspenseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { - client?: ApolloClient; - // (undocumented) +type UseSuspenseFragmentOptions = { + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; from: From; - // (undocumented) optimistic?: boolean; -} + client?: ApolloClient; +} & VariablesOption>; // @public (undocumented) export type UseSuspenseFragmentResult = { @@ -3207,11 +3205,11 @@ export interface UseSuspenseQueryResult = [ +export type VariablesOption = [ TVariables ] extends [never] ? { variables?: Record; -} : {} extends OnlyRequiredProperties ? { +} : Record extends OnlyRequiredProperties ? { variables?: TVariables; } : { variables: TVariables; @@ -3291,7 +3289,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/react/hooks/useBackgroundQuery.ts:51:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:75:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useLoadableQuery.ts:120:9 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useSuspenseFragment.ts:60:5 - (ae-forgotten-export) The symbol "From" needs to be exported by the entry point index.d.ts +// src/react/hooks/useSuspenseFragment.ts:70:5 - (ae-forgotten-export) The symbol "From" 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 89490c6022d..ec77a686633 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42235, + "dist/apollo-client.min.cjs": 42240, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34448 } diff --git a/src/react/hooks/__tests__/useSuspenseFragment.test.tsx b/src/react/hooks/__tests__/useSuspenseFragment.test.tsx index 108333917b9..d24d1804348 100644 --- a/src/react/hooks/__tests__/useSuspenseFragment.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseFragment.test.tsx @@ -10,6 +10,7 @@ import { Masked, MaskedDocumentNode, MaybeMasked, + OperationVariables, TypedDocumentNode, } from "../../../core"; import React, { Suspense } from "react"; @@ -1847,4 +1848,160 @@ describe.skip("type tests", () => { expectTypeOf(data).branded.toEqualTypeOf(); } }); + + test("variables are optional and can be anything with an untyped DocumentNode", () => { + const fragment = gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { foo: "bar" } }); + useSuspenseFragment({ fragment, from: null, variables: { bar: "baz" } }); + }); + + it("variables are optional and can be anything with unspecified TVariables on a TypedDocumentNode", () => { + const fragment: TypedDocumentNode<{ greeting: string }> = gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { foo: "bar" } }); + useSuspenseFragment({ fragment, from: null, variables: { bar: "baz" } }); + }); + + it("variables are optional and can be anything with OperationVariables on a TypedDocumentNode", () => { + const fragment: TypedDocumentNode< + { greeting: string }, + OperationVariables + > = gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { foo: "bar" } }); + useSuspenseFragment({ fragment, from: null, variables: { bar: "baz" } }); + }); + + it("variables are optional when TVariables are empty", () => { + const fragment: TypedDocumentNode< + { greeting: string }, + Record + > = gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + // @ts-expect-error unknown variable + useSuspenseFragment({ fragment, from: null, variables: { foo: "bar" } }); + }); + + it("does not allow variables when TVariables is `never`", () => { + const fragment: TypedDocumentNode<{ greeting: string }, never> = gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + // @ts-expect-error no variables argument allowed + useSuspenseFragment({ fragment, from: null, variables: { foo: "bar" } }); + }); + + it("optional variables are optional to useSuspenseFragment", () => { + const fragment: TypedDocumentNode<{ posts: string[] }, { limit?: number }> = + gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { limit: 10 } }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + it("enforces required variables when TVariables includes required variables", () => { + const fragment: TypedDocumentNode<{ character: string }, { id: string }> = + gql``; + + // @ts-expect-error missing variables argument + useSuspenseFragment({ fragment, from: null }); + // @ts-expect-error empty variables + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { id: "1" } }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + it("requires variables with mixed TVariables", () => { + const fragment: TypedDocumentNode< + { character: string }, + { id: string; language?: string } + > = gql``; + + // @ts-expect-error missing variables argument + useSuspenseFragment({ fragment, from: null }); + // @ts-expect-error empty variables + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { id: "1" } }); + useSuspenseFragment({ + fragment, + from: null, + // @ts-expect-error missing required variable + variables: { language: "en" }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { id: "1", language: "en" }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); }); diff --git a/src/react/hooks/useSuspenseFragment.ts b/src/react/hooks/useSuspenseFragment.ts index 79aed6c8ff0..646600b8c13 100644 --- a/src/react/hooks/useSuspenseFragment.ts +++ b/src/react/hooks/useSuspenseFragment.ts @@ -1,11 +1,12 @@ import type { ApolloClient, + DocumentNode, OperationVariables, Reference, StoreObject, + TypedDocumentNode, } from "../../core/index.js"; import { canonicalStringify } from "../../cache/index.js"; -import type { Cache } from "../../cache/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { getSuspenseCache } from "../internal/index.js"; import React, { useMemo } from "rehackt"; @@ -13,7 +14,7 @@ import type { FragmentKey } from "../internal/cache/types.js"; import { __use } from "./internal/__use.js"; import { wrapHook } from "./internal/index.js"; import type { FragmentType, MaybeMasked } from "../../masking/index.js"; -import type { NoInfer } from "../types/types.js"; +import type { NoInfer, VariablesOption } from "../types/types.js"; type From = | StoreObject @@ -22,15 +23,24 @@ type From = | string | null; -export interface UseSuspenseFragmentOptions - extends Omit< - Cache.DiffOptions, NoInfer>, - "id" | "query" | "optimistic" | "previousResult" | "returnPartialData" - >, - Omit< - Cache.ReadFragmentOptions, - "id" | "variables" | "returnPartialData" - > { +export type UseSuspenseFragmentOptions< + TData, + TVariables extends OperationVariables, +> = { + /** + * A GraphQL document created using the `gql` template string tag from + * `graphql-tag` with one or more fragments which will be used to determine + * the shape of data to read. If you provide more than one fragment in this + * document then you must also specify `fragmentName` to select a single. + */ + fragment: DocumentNode | TypedDocumentNode; + + /** + * The name of the fragment in your GraphQL document to be used. If you do + * not provide a `fragmentName` and there is only one fragment in your + * `fragment` document then that fragment will be used. + */ + fragmentName?: string; from: From; // Override this field to make it optional (default: true). optimistic?: boolean; @@ -43,7 +53,7 @@ export interface UseSuspenseFragmentOptions * @docGroup 1. Operation options */ client?: ApolloClient; -} +} & VariablesOption>; export type UseSuspenseFragmentResult = { data: MaybeMasked }; @@ -107,7 +117,7 @@ function useSuspenseFragment_< options: UseSuspenseFragmentOptions ): UseSuspenseFragmentResult { const client = useApolloClient(options.client); - const { from } = options; + const { from, variables } = options; const { cache } = client; const id = useMemo( @@ -120,10 +130,10 @@ function useSuspenseFragment_< const fragmentRef = id === null ? null : ( - getSuspenseCache(client).getFragmentRef( - [id, options.fragment, canonicalStringify(options.variables)], + getSuspenseCache(client).getFragmentRef( + [id, options.fragment, canonicalStringify(variables)], client, - { ...options, from: id } + { ...options, variables: variables as TVariables, from: id } ) ); diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 6389992519c..50b9e500579 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -15,25 +15,9 @@ import type { } from "../../utilities/index.js"; import { InternalQueryReference, wrapQueryRef } from "../internal/index.js"; import type { PreloadedQueryRef } from "../internal/index.js"; -import type { NoInfer } from "../index.js"; +import type { NoInfer, VariablesOption } from "../index.js"; import { wrapHook } from "../hooks/internal/index.js"; -type VariablesOption = - [TVariables] extends [never] ? - { - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ - variables?: Record; - } - : {} extends OnlyRequiredProperties ? - { - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ - variables?: TVariables; - } - : { - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ - variables: TVariables; - }; - export type PreloadQueryFetchPolicy = Extract< WatchQueryFetchPolicy, "cache-first" | "network-only" | "no-cache" | "cache-and-network" diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 42f90cec7de..5e82f4dc8fc 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -5,6 +5,7 @@ import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import type { Observable, ObservableSubscription, + OnlyRequiredProperties, } from "../../utilities/index.js"; import type { FetchResult } from "../../link/core/index.js"; import type { ApolloError } from "../../errors/index.js"; @@ -527,4 +528,20 @@ export interface SubscriptionCurrentObservable { subscription?: ObservableSubscription; } +export type VariablesOption = + [TVariables] extends [never] ? + { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables?: Record; + } + : Record extends OnlyRequiredProperties ? + { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables?: TVariables; + } + : { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables: TVariables; + }; + export type { NoInfer } from "../../utilities/index.js"; From b30b06cee0ad0d474dacd9b3d21796b55279a3b0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 16:52:03 +0000 Subject: [PATCH 31/68] Exit prerelease mode --- .changeset/pre.json | 18 ------------------ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 20a35fef3f9..00000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "mode": "pre", - "tag": "rc", - "initialVersions": { - "@apollo/client": "3.12.2" - }, - "changesets": [ - "blue-comics-train", - "bright-guests-chew", - "fluffy-worms-fail", - "heavy-pumas-boil", - "khaki-cars-develop", - "pretty-planets-cough", - "quiet-apricots-reply", - "sharp-windows-switch", - "tough-years-destroy" - ] -} diff --git a/package-lock.json b/package-lock.json index 413630b60cb..12913c788ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.13.0-rc.0", + "version": "3.12.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.13.0-rc.0", + "version": "3.12.11", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index efd9ef95ecc..73841878cf3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.13.0-rc.0", + "version": "3.12.11", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From e93d19fab897e6512a1a87cd9f3b6fbfb22cd574 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:10:44 -0700 Subject: [PATCH 32/68] Version Packages (#12372) --- .changeset/blue-comics-train.md | 7 ---- .changeset/bright-guests-chew.md | 16 -------- .changeset/fluffy-worms-fail.md | 5 --- .changeset/heavy-pumas-boil.md | 6 --- .changeset/khaki-cars-develop.md | 7 ---- .changeset/pretty-planets-cough.md | 24 ----------- .changeset/quiet-apricots-reply.md | 6 --- .changeset/sharp-windows-switch.md | 5 --- .changeset/tough-years-destroy.md | 5 --- CHANGELOG.md | 64 ++++++++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 2 +- 12 files changed, 67 insertions(+), 84 deletions(-) delete mode 100644 .changeset/blue-comics-train.md delete mode 100644 .changeset/bright-guests-chew.md delete mode 100644 .changeset/fluffy-worms-fail.md delete mode 100644 .changeset/heavy-pumas-boil.md delete mode 100644 .changeset/khaki-cars-develop.md delete mode 100644 .changeset/pretty-planets-cough.md delete mode 100644 .changeset/quiet-apricots-reply.md delete mode 100644 .changeset/sharp-windows-switch.md delete mode 100644 .changeset/tough-years-destroy.md diff --git a/.changeset/blue-comics-train.md b/.changeset/blue-comics-train.md deleted file mode 100644 index 533df0e9022..00000000000 --- a/.changeset/blue-comics-train.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@apollo/client": minor ---- - -Adds a new `useSuspenseFragment` hook. - -`useSuspenseFragment` suspends until `data` is complete. It is a drop-in replacement for `useFragment` when you prefer to use Suspense to control the loading state of a fragment. diff --git a/.changeset/bright-guests-chew.md b/.changeset/bright-guests-chew.md deleted file mode 100644 index 2d4c565a396..00000000000 --- a/.changeset/bright-guests-chew.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fix the return type of the `updateQuery` function to allow for `undefined`. `updateQuery` had the ability to bail out of the update by returning a falsey value, but the return type enforced a query value. - -```ts -observableQuery.updateQuery((unsafePreviousData, { previousData, complete }) => { - if (!complete) { - // Bail out of the update by returning early - return; - } - - // ... -}); -``` diff --git a/.changeset/fluffy-worms-fail.md b/.changeset/fluffy-worms-fail.md deleted file mode 100644 index b232d671f0e..00000000000 --- a/.changeset/fluffy-worms-fail.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Ensure errors thrown in the `onCompleted` callback from `useMutation` don't call `onError`. diff --git a/.changeset/heavy-pumas-boil.md b/.changeset/heavy-pumas-boil.md deleted file mode 100644 index 8c523755db0..00000000000 --- a/.changeset/heavy-pumas-boil.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@apollo/client": minor ---- - -Deprecate the `onCompleted` and `onError` callbacks of `useQuery` and `useLazyQuery`. -For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. diff --git a/.changeset/khaki-cars-develop.md b/.changeset/khaki-cars-develop.md deleted file mode 100644 index 09022ce6b89..00000000000 --- a/.changeset/khaki-cars-develop.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@apollo/client": patch ---- - -Deprecate option `ignoreResults` in `useMutation`. -Once this option is removed, existing code still using it might see increase in re-renders. -If you don't want to synchronize your component state with the mutation, please use `useApolloClient` to get your ApolloClient instance and call `client.mutate` directly. diff --git a/.changeset/pretty-planets-cough.md b/.changeset/pretty-planets-cough.md deleted file mode 100644 index b7b76ba4aa6..00000000000 --- a/.changeset/pretty-planets-cough.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -"@apollo/client": minor ---- - -Provide a more type-safe option for the previous data value passed to `observableQuery.updateQuery`. Using it could result in crashes at runtime as this callback could be called with partial data even though its type reported the value as a complete result. - -The `updateQuery` callback function is now called with a new type-safe `previousData` property and a new `complete` property in the 2nd argument that determines whether `previousData` is a complete or partial result. - -As a result of this change, it is recommended to use the `previousData` property passed to the 2nd argument of the callback rather than using the previous data value from the first argument since that value is not type-safe. The first argument is now deprecated and will be removed in a future version of Apollo Client. - -```ts -observableQuery.updateQuery((unsafePreviousData, { previousData, complete }) => { - previousData - // ^? TData | DeepPartial | undefined - - if (complete) { - previousData - // ^? TData - } else { - previousData - // ^? DeepPartial | undefined - } -}) -``` diff --git a/.changeset/quiet-apricots-reply.md b/.changeset/quiet-apricots-reply.md deleted file mode 100644 index 2d52fd9f0d0..00000000000 --- a/.changeset/quiet-apricots-reply.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"@apollo/client": patch ---- - -In case of a multipart response (e.g. with `@defer`), query deduplication will -now keep going until the final chunk has been received. diff --git a/.changeset/sharp-windows-switch.md b/.changeset/sharp-windows-switch.md deleted file mode 100644 index 7b47f0c978b..00000000000 --- a/.changeset/sharp-windows-switch.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": minor ---- - -Reject the mutation promise if errors are thrown in the `onCompleted` callback of `useMutation`. diff --git a/.changeset/tough-years-destroy.md b/.changeset/tough-years-destroy.md deleted file mode 100644 index 90dfab53bb8..00000000000 --- a/.changeset/tough-years-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fix the type of the `variables` property passed as the 2nd argument to the `subscribeToMore` callback. This was previously reported as the `variables` type for the subscription itself, but is now properly typed as the query `variables`. diff --git a/CHANGELOG.md b/CHANGELOG.md index db3af4247c1..e42bde46049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,69 @@ # @apollo/client +## 3.13.0 + +### Minor Changes + +- [#12066](https://github.com/apollographql/apollo-client/pull/12066) [`c01da5d`](https://github.com/apollographql/apollo-client/commit/c01da5da639d4d9e882d380573b7876df4a1d65b) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds a new `useSuspenseFragment` hook. + + `useSuspenseFragment` suspends until `data` is complete. It is a drop-in replacement for `useFragment` when you prefer to use Suspense to control the loading state of a fragment. See the [documentation](https://www.apollographql.com/docs/react/data/fragments#usesuspensefragment) for more details. + +- [#12174](https://github.com/apollographql/apollo-client/pull/12174) [`ba5cc33`](https://github.com/apollographql/apollo-client/commit/ba5cc330f8734a989eef71e883861f848388ac0c) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Ensure errors thrown in the `onCompleted` callback from `useMutation` don't call `onError`. + +- [#12340](https://github.com/apollographql/apollo-client/pull/12340) [`716d02e`](https://github.com/apollographql/apollo-client/commit/716d02ec9c5b1448f50cb50a0306a345310a2342) Thanks [@phryneas](https://github.com/phryneas)! - Deprecate the `onCompleted` and `onError` callbacks of `useQuery` and `useLazyQuery`. + For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Provide a more type-safe option for the previous data value passed to `observableQuery.updateQuery`. Using it could result in crashes at runtime as this callback could be called with partial data even though its type reported the value as a complete result. + + The `updateQuery` callback function is now called with a new type-safe `previousData` property and a new `complete` property in the 2nd argument that determines whether `previousData` is a complete or partial result. + + As a result of this change, it is recommended to use the `previousData` property passed to the 2nd argument of the callback rather than using the previous data value from the first argument since that value is not type-safe. The first argument is now deprecated and will be removed in a future version of Apollo Client. + + ```ts + observableQuery.updateQuery( + (unsafePreviousData, { previousData, complete }) => { + previousData; + // ^? TData | DeepPartial | undefined + + if (complete) { + previousData; + // ^? TData + } else { + previousData; + // ^? DeepPartial | undefined + } + } + ); + ``` + +- [#12174](https://github.com/apollographql/apollo-client/pull/12174) [`ba5cc33`](https://github.com/apollographql/apollo-client/commit/ba5cc330f8734a989eef71e883861f848388ac0c) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Reject the mutation promise if errors are thrown in the `onCompleted` callback of `useMutation`. + +### Patch Changes + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Fix the return type of the `updateQuery` function to allow for `undefined`. `updateQuery` had the ability to bail out of the update by returning a falsey value, but the return type enforced a query value. + + ```ts + observableQuery.updateQuery( + (unsafePreviousData, { previousData, complete }) => { + if (!complete) { + // Bail out of the update by returning early + return; + } + + // ... + } + ); + ``` + +- [#12296](https://github.com/apollographql/apollo-client/pull/12296) [`2422df2`](https://github.com/apollographql/apollo-client/commit/2422df202a7ec71365d5a8ab5b3b554fcf60e4af) Thanks [@Cellule](https://github.com/Cellule)! - Deprecate option `ignoreResults` in `useMutation`. + Once this option is removed, existing code still using it might see increase in re-renders. + If you don't want to synchronize your component state with the mutation, please use `useApolloClient` to get your ApolloClient instance and call `client.mutate` directly. + +- [#12338](https://github.com/apollographql/apollo-client/pull/12338) [`67c16c9`](https://github.com/apollographql/apollo-client/commit/67c16c93897e36be980ba2139ee8bd3f24ab8558) Thanks [@phryneas](https://github.com/phryneas)! - In case of a multipart response (e.g. with `@defer`), query deduplication will + now keep going until the final chunk has been received. + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Fix the type of the `variables` property passed as the 2nd argument to the `subscribeToMore` callback. This was previously reported as the `variables` type for the subscription itself, but is now properly typed as the query `variables`. + ## 3.13.0-rc.0 ### Minor Changes diff --git a/package-lock.json b/package-lock.json index 12913c788ed..c4096748524 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.12.11", + "version": "3.13.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.12.11", + "version": "3.13.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 73841878cf3..9bcc00106b3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.12.11", + "version": "3.13.0", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From bdfc5b2e386ed5f835716a542de0cf17da37f7fc Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 14 Feb 2025 10:56:18 +0100 Subject: [PATCH 33/68] `ObervableQuery.refetch`: don't refetch with `cache-and-network`, swich to `network-only` instead (#12369) * `ObervableQuery.refetch`: don't refetch with `cache-and-network`, swich to `network-only` instead * update test * Update src/core/__tests__/ApolloClient/general.test.ts Co-authored-by: Jerel Miller * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: Jerel Miller Co-authored-by: phryneas <4282439+phryneas@users.noreply.github.com> --- .changeset/fresh-spies-burn.md | 5 + .size-limits.json | 4 +- src/__tests__/client.ts | 3 - src/core/ObservableQuery.ts | 4 +- .../__tests__/ApolloClient/general.test.ts | 233 ++++++++++++++---- src/core/__tests__/ObservableQuery.ts | 3 +- 6 files changed, 195 insertions(+), 57 deletions(-) create mode 100644 .changeset/fresh-spies-burn.md diff --git a/.changeset/fresh-spies-burn.md b/.changeset/fresh-spies-burn.md new file mode 100644 index 00000000000..bd29c72aeff --- /dev/null +++ b/.changeset/fresh-spies-burn.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`ObervableQuery.refetch`: don't refetch with `cache-and-network`, swich to `network-only` instead diff --git a/.size-limits.json b/.size-limits.json index ec77a686633..2c9d084c1c3 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42240, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34448 + "dist/apollo-client.min.cjs": 42232, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34438 } diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index 7e99eed1c6a..bef271b8ebb 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -3382,9 +3382,6 @@ describe("@connection", () => { // nextFetchPolicy function ends up getting called twice. void obs.refetch(); - await expect(stream).toEmitMatchedValue({ data: { count: "initial" } }); - expect(nextFetchPolicyCallCount).toBe(2); - await expect(stream).toEmitMatchedValue({ data: { count: 0 } }); expect(nextFetchPolicyCallCount).toBe(2); diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 78e1ebd2304..2f3e5c785de 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -404,9 +404,7 @@ export class ObservableQuery< // (no-cache, network-only, or cache-and-network), override it with // network-only to force the refetch for this fetchQuery call. const { fetchPolicy } = this.options; - if (fetchPolicy === "cache-and-network") { - reobserveOptions.fetchPolicy = fetchPolicy; - } else if (fetchPolicy === "no-cache") { + if (fetchPolicy === "no-cache") { reobserveOptions.fetchPolicy = "no-cache"; } else { reobserveOptions.fetchPolicy = "network-only"; diff --git a/src/core/__tests__/ApolloClient/general.test.ts b/src/core/__tests__/ApolloClient/general.test.ts index a11cdde119b..6c7814ca1df 100644 --- a/src/core/__tests__/ApolloClient/general.test.ts +++ b/src/core/__tests__/ApolloClient/general.test.ts @@ -24,7 +24,10 @@ import { // core import { NetworkStatus } from "../../networkStatus"; -import { WatchQueryOptions } from "../../watchQueryOptions"; +import { + WatchQueryFetchPolicy, + WatchQueryOptions, +} from "../../watchQueryOptions"; import { QueryManager } from "../../QueryManager"; import { ApolloError } from "../../../errors"; @@ -32,7 +35,7 @@ import { ApolloError } from "../../../errors"; // testing utils import { waitFor } from "@testing-library/react"; import { wait } from "../../../testing/core"; -import { ApolloClient } from "../../../core"; +import { ApolloClient, ApolloQueryResult } from "../../../core"; import { mockFetchQuery } from "../ObservableQuery"; import { Concast, print } from "../../../utilities"; import { @@ -873,23 +876,7 @@ describe("ApolloClient", () => { expect(data).toBe(observable.getCurrentResult().data); }); - it("sets networkStatus to `refetch` when refetching", async () => { - const request: WatchQueryOptions = { - query: gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name - } - } - `, - variables: { - id: "1", - }, - notifyOnNetworkStatusChange: true, - // This causes a loading:true result to be delivered from the cache - // before the final data2 result is delivered. - fetchPolicy: "cache-and-network", - }; + { const data1 = { people_one: { name: "Luke Skywalker", @@ -901,37 +888,187 @@ describe("ApolloClient", () => { name: "Luke Skywalker has a new name", }, }; + it.each< + [ + notifyOnNetworkStatusChange: boolean, + fetchPolicy: WatchQueryFetchPolicy, + expectedInitialResults: ApolloQueryResult[], + expectedRefetchedResults: ApolloQueryResult[], + ] + >([ + [ + false, + "cache-first", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + [ + false, + "network-only", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + [ + false, + "cache-and-network", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + // { + // data: data1, + // loading: true, + // networkStatus: NetworkStatus.refetch, + // partial: false, + // }, + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + [ + true, + "cache-first", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + { + data: data1, + loading: true, + networkStatus: NetworkStatus.refetch, + }, + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + [ + true, + "network-only", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + { + data: data1, + loading: true, + networkStatus: NetworkStatus.refetch, + }, + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + [ + true, + "cache-and-network", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + { + data: data1, + loading: true, + networkStatus: NetworkStatus.refetch, + }, + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + ])( + "networkStatus changes (notifyOnNetworkStatusChange: %s, fetchPolicy %s)", + async ( + notifyOnNetworkStatusChange, + fetchPolicy, + expectedInitialResults, + expectedRefetchedResults + ) => { + const request: WatchQueryOptions = { + query: gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name + } + } + `, + variables: { + id: "1", + }, + notifyOnNetworkStatusChange, + fetchPolicy, + }; - const client = new ApolloClient({ - cache: new InMemoryCache({ addTypename: false }), - link: new MockLink([ - { request, result: { data: data1 } }, - { request, result: { data: data2 } }, - ]), - }); - - const observable = client.watchQuery(request); - const stream = new ObservableStream(observable); - - await expect(stream).toEmitApolloQueryResult({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - }); - - void observable.refetch(); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { request, result: { data: data1 } }, + { request, result: { data: data2 } }, + ]), + }); - await expect(stream).toEmitApolloQueryResult({ - data: data1, - loading: true, - networkStatus: NetworkStatus.refetch, - }); - await expect(stream).toEmitApolloQueryResult({ - data: data2, - loading: false, - networkStatus: NetworkStatus.ready, - }); - }); + const observable = client.watchQuery(request); + const stream = new ObservableStream(observable); + for (const expected of expectedInitialResults) { + await expect(stream).toEmitApolloQueryResult(expected); + } + void observable.refetch(); + for (const expected of expectedRefetchedResults) { + await expect(stream).toEmitApolloQueryResult(expected); + } + expect(stream).not.toEmitAnything(); + } + ); + } it("allows you to refetch queries with promises", async () => { const request = { diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index c816957a898..f367b11cb55 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -1684,6 +1684,7 @@ describe("ObservableQuery", () => { query, fetchPolicy: "cache-and-network", returnPartialData: true, + notifyOnNetworkStatusChange: true, }); const stream = new ObservableStream(observable); @@ -1724,7 +1725,7 @@ describe("ObservableQuery", () => { expect(result).toEqualApolloQueryResult({ data: { - counter: 5, + counter: 4, name: "Ben", }, loading: false, From d3f8f130718ef50531ca0079192c2672a513814a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 14 Feb 2025 11:26:29 -0700 Subject: [PATCH 34/68] Export `UseSuspenseFragmentOptions` (#12375) --- .api-reports/api-report-react.api.md | 4 +--- .api-reports/api-report-react_hooks.api.md | 4 +--- .api-reports/api-report.api.md | 4 +--- .changeset/shiny-sheep-behave.md | 5 +++++ .size-limits.json | 4 ++-- src/react/hooks/index.ts | 5 ++++- 6 files changed, 14 insertions(+), 12 deletions(-) create mode 100644 .changeset/shiny-sheep-behave.md diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 380070343f9..b2aae34e238 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -2450,8 +2450,6 @@ export function useSubscription(options: UseSuspenseFragmentOptions & { from: NonNullable>; @@ -2471,7 +2469,7 @@ export function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; // @public (undocumented) -type UseSuspenseFragmentOptions = { +export type UseSuspenseFragmentOptions = { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: From; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index 5a283fea30b..59ef810efca 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -2285,8 +2285,6 @@ export function useSubscription(options: UseSuspenseFragmentOptions & { from: NonNullable>; @@ -2308,7 +2306,7 @@ export function useSuspenseFragment = { +export type UseSuspenseFragmentOptions = { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: From; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 3c3a0f2d981..cfb416b8fc8 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -3111,8 +3111,6 @@ export function useSubscription(options: UseSuspenseFragmentOptions & { from: NonNullable>; @@ -3132,7 +3130,7 @@ export function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; // @public (undocumented) -type UseSuspenseFragmentOptions = { +export type UseSuspenseFragmentOptions = { fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: From; diff --git a/.changeset/shiny-sheep-behave.md b/.changeset/shiny-sheep-behave.md new file mode 100644 index 00000000000..67597141cd3 --- /dev/null +++ b/.changeset/shiny-sheep-behave.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Export the `UseSuspenseFragmentOptions` type. diff --git a/.size-limits.json b/.size-limits.json index 2c9d084c1c3..877bcc84600 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42232, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34438 + "dist/apollo-client.min.cjs": 42231, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34437 } diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 3fe6ccbc27d..5f0ac41c4b4 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -11,7 +11,10 @@ export type { UseSuspenseQueryResult } from "./useSuspenseQuery.js"; export { useSuspenseQuery } from "./useSuspenseQuery.js"; export type { UseBackgroundQueryResult } from "./useBackgroundQuery.js"; export { useBackgroundQuery } from "./useBackgroundQuery.js"; -export type { UseSuspenseFragmentResult } from "./useSuspenseFragment.js"; +export type { + UseSuspenseFragmentResult, + UseSuspenseFragmentOptions, +} from "./useSuspenseFragment.js"; export { useSuspenseFragment } from "./useSuspenseFragment.js"; export type { LoadQueryFunction, From 356fcc9414286988d884e26dffafaf4d317d1b2d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Feb 2025 11:32:33 -0700 Subject: [PATCH 35/68] Version Packages (#12377) Co-authored-by: github-actions[bot] --- .changeset/fresh-spies-burn.md | 5 ----- .changeset/shiny-sheep-behave.md | 5 ----- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 .changeset/fresh-spies-burn.md delete mode 100644 .changeset/shiny-sheep-behave.md diff --git a/.changeset/fresh-spies-burn.md b/.changeset/fresh-spies-burn.md deleted file mode 100644 index bd29c72aeff..00000000000 --- a/.changeset/fresh-spies-burn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -`ObervableQuery.refetch`: don't refetch with `cache-and-network`, swich to `network-only` instead diff --git a/.changeset/shiny-sheep-behave.md b/.changeset/shiny-sheep-behave.md deleted file mode 100644 index 67597141cd3..00000000000 --- a/.changeset/shiny-sheep-behave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Export the `UseSuspenseFragmentOptions` type. diff --git a/CHANGELOG.md b/CHANGELOG.md index e42bde46049..3a7e6f9e7ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # @apollo/client +## 3.13.1 + +### Patch Changes + +- [#12369](https://github.com/apollographql/apollo-client/pull/12369) [`bdfc5b2`](https://github.com/apollographql/apollo-client/commit/bdfc5b2e386ed5f835716a542de0cf17da37f7fc) Thanks [@phryneas](https://github.com/phryneas)! - `ObervableQuery.refetch`: don't refetch with `cache-and-network`, swich to `network-only` instead + +- [#12375](https://github.com/apollographql/apollo-client/pull/12375) [`d3f8f13`](https://github.com/apollographql/apollo-client/commit/d3f8f130718ef50531ca0079192c2672a513814a) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Export the `UseSuspenseFragmentOptions` type. + ## 3.13.0 ### Minor Changes diff --git a/package-lock.json b/package-lock.json index c4096748524..7e468e67dd0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.13.0", + "version": "3.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.13.0", + "version": "3.13.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 9bcc00106b3..e265d33c34a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.13.0", + "version": "3.13.1", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 3c8075eb22b3e6ef0e25f0309ea5f6be28febb5c Mon Sep 17 00:00:00 2001 From: Alan Johnson Date: Mon, 3 Mar 2025 15:53:23 -0400 Subject: [PATCH 36/68] Docs: simplify subscribeToMore example (#12381) --- docs/source/data/subscriptions.mdx | 130 +++++++++++++---------------- 1 file changed, 57 insertions(+), 73 deletions(-) diff --git a/docs/source/data/subscriptions.mdx b/docs/source/data/subscriptions.mdx index 3f3fed9fcac..2f53a8caa63 100644 --- a/docs/source/data/subscriptions.mdx +++ b/docs/source/data/subscriptions.mdx @@ -460,69 +460,75 @@ const COMMENTS_SUBSCRIPTION = gql` -Next, we modify our `CommentsPageWithData` function to add a `subscribeToNewComments` property to the `CommentsPage` component it returns. This property is a function that will be responsible for calling `subscribeToMore` after the component mounts. +Next, we modify our `CommentsPageWithData` component to call `subscribeToMore` after the comments query loads. ```tsx {10-25} function CommentsPageWithData({ params }: CommentsPageWithDataProps) { - const { subscribeToMore, ...result } = useQuery( - COMMENTS_QUERY, - { variables: { postID: params.postID } } - ); + const { subscribeToMore, ...result } = useQuery(COMMENTS_QUERY, { + variables: { postID: params.postID }, + }); + + useEffect(() => { + // This assumes you want to wait to start the subscription + // after the query has loaded. + if (result.data) { + const unsubscribe = subscribeToMore({ + document: COMMENTS_SUBSCRIPTION, + variables: { postID: params.postID }, + updateQuery: (prev, { subscriptionData }) => { + if (!subscriptionData.data) return prev; + const newFeedItem = subscriptionData.data.commentAdded; + + return Object.assign({}, prev, { + post: { + comments: [newFeedItem, ...prev.post.comments], + }, + }); + }, + }); + + return () => { + unsubscribe(); + }; + } + }, [result.data, params.postID, subscribeToMore]); - return ( - - subscribeToMore({ - document: COMMENTS_SUBSCRIPTION, - variables: { postID: params.postID }, - updateQuery: (prev, { subscriptionData }) => { - if (!subscriptionData.data) return prev; - const newFeedItem = subscriptionData.data.commentAdded; - - return Object.assign({}, prev, { - post: { - comments: [newFeedItem, ...prev.post.comments] - } - }); - } - }) - } - /> - ); + return ; } ``` ```jsx {10-25} function CommentsPageWithData({ params }) { - const { subscribeToMore, ...result } = useQuery( - COMMENTS_QUERY, - { variables: { postID: params.postID } } - ); + const { subscribeToMore, ...result } = useQuery(COMMENTS_QUERY, { + variables: { postID: params.postID }, + }); + + useEffect(() => { + if (result.data) { + const unsubscribe = subscribeToMore({ + document: COMMENTS_SUBSCRIPTION, + variables: { postID: params.postID }, + updateQuery: (prev, { subscriptionData }) => { + if (!subscriptionData.data) return prev; + const newFeedItem = subscriptionData.data.commentAdded; + + return Object.assign({}, prev, { + post: { + comments: [newFeedItem, ...prev.post.comments], + }, + }); + }, + }); + + return () => { + unsubscribe(); + }; + } + }, [result.data, params.postID]); - return ( - - subscribeToMore({ - document: COMMENTS_SUBSCRIPTION, - variables: { postID: params.postID }, - updateQuery: (prev, { subscriptionData }) => { - if (!subscriptionData.data) return prev; - const newFeedItem = subscriptionData.data.commentAdded; - - return Object.assign({}, prev, { - post: { - comments: [newFeedItem, ...prev.post.comments] - } - }); - } - }) - } - /> - ); + return ; } ``` @@ -534,28 +540,6 @@ In the example above, we pass three options to `subscribeToMore`: * `variables` indicates the variables to include when executing the subscription. * `updateQuery` is a function that tells Apollo Client how to combine the query's currently cached result (`prev`) with the `subscriptionData` that's pushed by our GraphQL server. The return value of this function **completely replaces** the current cached result for the query. -Finally, in our definition of `CommentsPage`, we tell the component to `subscribeToNewComments` when it mounts: - - - -```tsx -export function CommentsPage({ subscribeToNewComments }: CommentsPageProps) { - useEffect(() => subscribeToNewComments(), []); - - return <>... -} -``` - -```jsx -export function CommentsPage({ subscribeToNewComments }) { - useEffect(() => subscribeToNewComments(), []); - - return <>... -} -``` - - - ## `useSubscription` API reference > **Note:** If you're using React Apollo's `Subscription` render prop component, the option/result details listed below are still valid (options are component props and results are passed into the render prop function). The only difference is that a `subscription` prop (which holds a GraphQL subscription document parsed into an AST by `gql`) is also required. From b11773aeaecce1019d1fd6c45d38e0371f20f22f Mon Sep 17 00:00:00 2001 From: Hamid Yaftian Date: Mon, 3 Mar 2025 23:23:49 +0330 Subject: [PATCH 37/68] update webpack.md with a link to working jest transformer (#12395) --- docs/source/integrations/webpack.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/integrations/webpack.md b/docs/source/integrations/webpack.md index 99a578b99b0..12083ae97cd 100644 --- a/docs/source/integrations/webpack.md +++ b/docs/source/integrations/webpack.md @@ -47,7 +47,7 @@ export default graphql(currentUserQuery)(Profile) ## Jest -[Jest](https://facebook.github.io/jest/) can't use the Webpack loaders. To make the same transformation work in Jest, use [jest-transform-graphql](https://github.com/remind101/jest-transform-graphql). +[Jest](https://facebook.github.io/jest/) can't use the Webpack loaders. To make the same transformation work in Jest, use [jest-graphql-transformer](https://github.com/hamidyfine/jest-graphql-transformer). ## FuseBox From 6fcad3bc933d2e87dfef44fbb65c92ab37a2fa72 Mon Sep 17 00:00:00 2001 From: camillelawrence Date: Wed, 5 Mar 2025 12:49:46 -0500 Subject: [PATCH 38/68] Update README.md with 2025 Summit Announcement (#12401) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 404b83bc620..abf10b2c149 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ --- **Announcement:** -Join 1000+ engineers at GraphQL Summit for talks, workshops, and office hours, Oct 8-10 in NYC. [Get your pass here ->](https://summit.graphql.com/?utm_campaign=github_federation_readme) +Join 1000+ engineers at GraphQL Summit 2025 by Apollo for talks, workshops, and office hours. Oct 6-8, 2025 in San Francisco. [Get your pass here ->](https://www.apollographql.com/graphql-summit-2025?utm_campaign=2025-03-04_graphql-summit-github-announcement&utm_medium=github&utm_source=apollo-server) --- From 644bb2662168a9bac0519be6979f0db38b0febc4 Mon Sep 17 00:00:00 2001 From: Joshua Smee <40449048+Joja81@users.noreply.github.com> Date: Thu, 6 Mar 2025 09:05:40 +1100 Subject: [PATCH 39/68] Allow DeepOmit to support partial (#12392) --- .changeset/grumpy-donuts-smoke.md | 5 +++++ src/utilities/types/DeepOmit.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/grumpy-donuts-smoke.md diff --git a/.changeset/grumpy-donuts-smoke.md b/.changeset/grumpy-donuts-smoke.md new file mode 100644 index 00000000000..d6bcb73a182 --- /dev/null +++ b/.changeset/grumpy-donuts-smoke.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fixes an issue where the DeepOmit type would turn optional properties into required properties. This should only affect you if you were using the omitDeep or stripTypename utilities exported by Apollo Client. diff --git a/src/utilities/types/DeepOmit.ts b/src/utilities/types/DeepOmit.ts index 9d1e31e6817..6a850dbc06c 100644 --- a/src/utilities/types/DeepOmit.ts +++ b/src/utilities/types/DeepOmit.ts @@ -22,7 +22,7 @@ export type DeepOmitArray = { export type DeepOmit = T extends DeepOmitPrimitive ? T : { - [P in Exclude]: T[P] extends infer TP ? + [P in keyof T as P extends K ? never : P]: T[P] extends infer TP ? TP extends DeepOmitPrimitive ? TP : TP extends any[] ? DeepOmitArray : DeepOmit From 4332b886f0409145af57f26d334f86e5a1b465c5 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 6 Mar 2025 10:59:42 -0700 Subject: [PATCH 40/68] Show `NaN` in `MockLink` debug messages for unmatched variables (#12404) --- .api-reports/api-report-utilities.api.md | 2 +- .changeset/rich-adults-destroy.md | 5 +++ .../__tests__/__snapshots__/mockLink.ts.snap | 14 ++++++++ .../core/mocking/__tests__/mockLink.ts | 34 ++++++++++++++++++- src/testing/core/mocking/mockLink.ts | 33 ++++++++++++++++-- 5 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 .changeset/rich-adults-destroy.md create mode 100644 src/testing/core/mocking/__tests__/__snapshots__/mockLink.ts.snap diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index 59b4ba4b1ba..33d2d83be42 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -715,7 +715,7 @@ export class DeepMerger { // // @public (undocumented) export type DeepOmit = T extends DeepOmitPrimitive ? T : { - [P in Exclude]: T[P] extends infer TP ? TP extends DeepOmitPrimitive ? TP : TP extends any[] ? DeepOmitArray : DeepOmit : never; + [P in keyof T as P extends K ? never : P]: T[P] extends infer TP ? TP extends DeepOmitPrimitive ? TP : TP extends any[] ? DeepOmitArray : DeepOmit : never; }; // @public (undocumented) diff --git a/.changeset/rich-adults-destroy.md b/.changeset/rich-adults-destroy.md new file mode 100644 index 00000000000..51ca5fac23f --- /dev/null +++ b/.changeset/rich-adults-destroy.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Show `NaN` rather than converting to `null` in debug messages from `MockLink` for unmatched `variables` values. diff --git a/src/testing/core/mocking/__tests__/__snapshots__/mockLink.ts.snap b/src/testing/core/mocking/__tests__/__snapshots__/mockLink.ts.snap new file mode 100644 index 00000000000..c25d45c175e --- /dev/null +++ b/src/testing/core/mocking/__tests__/__snapshots__/mockLink.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`shows undefined and NaN in debug messages 1`] = ` +"No more mocked responses for the query: query ($id: ID!, $filter: Boolean) { + usersByTestId(id: $id, filter: $filter) { + id + } +} +Expected variables: {\\"id\\":NaN,\\"filter\\":} + +Failed to match 1 mock for this query. The mocked response had the following variables: + {\\"id\\":1,\\"filter\\":true} +" +`; diff --git a/src/testing/core/mocking/__tests__/mockLink.ts b/src/testing/core/mocking/__tests__/mockLink.ts index fe618ae4f48..961c745ce70 100644 --- a/src/testing/core/mocking/__tests__/mockLink.ts +++ b/src/testing/core/mocking/__tests__/mockLink.ts @@ -1,7 +1,11 @@ import gql from "graphql-tag"; import { MockLink, MockedResponse } from "../mockLink"; import { execute } from "../../../../link/core/execute"; -import { ObservableStream, enableFakeTimers } from "../../../internal"; +import { + ObservableStream, + enableFakeTimers, + spyOnConsole, +} from "../../../internal"; describe("MockedResponse.newData", () => { const setup = () => { @@ -357,6 +361,34 @@ test("removes fields with @client directives", async () => { } }); +test("shows undefined and NaN in debug messages", async () => { + using _ = spyOnConsole("warn"); + + const query = gql` + query ($id: ID!, $filter: Boolean) { + usersByTestId(id: $id, filter: $filter) { + id + } + } + `; + + const link = new MockLink([ + { + request: { query, variables: { id: 1, filter: true } }, + // The actual response data makes no difference in this test + result: { data: { usersByTestId: null } }, + }, + ]); + + const stream = new ObservableStream( + execute(link, { query, variables: { id: NaN, filter: undefined } }) + ); + + const error = await stream.takeError(); + + expect(error.message).toMatchSnapshot(); +}); + describe.skip("type tests", () => { const ANY = {} as any; test("covariant behaviour: `MockedResponses` should be assignable to `MockedResponse`", () => { diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index 898efe6bfd3..dfae6ed60b0 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -14,12 +14,12 @@ import { addTypenameToDocument, removeClientSetsFromDocument, cloneDeep, - stringifyForDisplay, print, getOperationDefinition, getDefaultValues, removeDirectivesFromDocument, checkDocument, + makeUniqueId, } from "../../../utilities/index.js"; import type { Unmasked } from "../../../masking/index.js"; @@ -136,14 +136,14 @@ export class MockLink extends ApolloLink { if (!response) { configError = new Error( `No more mocked responses for the query: ${print(operation.query)} -Expected variables: ${stringifyForDisplay(operation.variables)} +Expected variables: ${stringifyForDebugging(operation.variables)} ${ unmatchedVars.length > 0 ? ` Failed to match ${unmatchedVars.length} mock${ unmatchedVars.length === 1 ? "" : "s" } for this query. The mocked response had the following variables: -${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} +${unmatchedVars.map((d) => ` ${stringifyForDebugging(d)}`).join("\n")} ` : "" }` @@ -280,3 +280,30 @@ export function mockSingleLink(...mockedResponses: Array): MockApolloLink { return new MockLink(mocks, maybeTypename); } + +// This is similiar to the stringifyForDisplay utility we ship, but includes +// support for NaN in addition to undefined. More values may be handled in the +// future. This is not added to the primary stringifyForDisplay helper since it +// is used for the cache and other purposes. We need this for debuggging only. +export function stringifyForDebugging(value: any, space = 0): string { + const undefId = makeUniqueId("undefined"); + const nanId = makeUniqueId("NaN"); + + return JSON.stringify( + value, + (_, value) => { + if (value === void 0) { + return undefId; + } + + if (Number.isNaN(value)) { + return nanId; + } + + return value; + }, + space + ) + .replace(new RegExp(JSON.stringify(undefId), "g"), "") + .replace(new RegExp(JSON.stringify(nanId), "g"), "NaN"); +} From 6aa2f3e81ee0ae59da7ae0b12000bb5a55ec5c6d Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 6 Mar 2025 19:08:11 +0100 Subject: [PATCH 41/68] turn queryRef symbols into `Symbol.for` calls (#12409) * turn queryRef symbols into `Symbol.for` calls * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: phryneas <4282439+phryneas@users.noreply.github.com> --- .changeset/grumpy-dogs-add.md | 5 +++++ .size-limits.json | 2 +- src/react/hooks/useLazyQuery.ts | 5 +++-- src/react/hooks/useQuery.ts | 4 ++-- src/react/internal/cache/QueryReference.ts | 6 ++++-- 5 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 .changeset/grumpy-dogs-add.md diff --git a/.changeset/grumpy-dogs-add.md b/.changeset/grumpy-dogs-add.md new file mode 100644 index 00000000000..227311db800 --- /dev/null +++ b/.changeset/grumpy-dogs-add.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +To mitigate problems when Apollo Client ends up more than once in the bundle, some unique symbols were converted into `Symbol.for` calls. diff --git a/.size-limits.json b/.size-limits.json index 877bcc84600..ebe7da3770a 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42231, + "dist/apollo-client.min.cjs": 42243, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34437 } diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index e22b13fb0d4..3f51b242fc6 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -5,6 +5,7 @@ import * as React from "rehackt"; import type { ApolloClient, ApolloQueryResult, + ObservableQuery, OperationVariables, WatchQueryOptions, } from "../../core/index.js"; @@ -17,7 +18,7 @@ import type { QueryHookOptions, QueryResult, } from "../types/types.js"; -import type { InternalResult, ObsQueryWithMeta } from "./useQuery.js"; +import type { InternalResult } from "./useQuery.js"; import { createMakeWatchQueryOptions, getDefaultFetchPolicy, @@ -203,7 +204,7 @@ export function useLazyQuery< function executeQuery( resultData: InternalResult, - observable: ObsQueryWithMeta, + observable: ObservableQuery, client: ApolloClient, currentQuery: DocumentNode, options: QueryHookOptions & { diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 764c94cb377..7e81ce8feba 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -68,9 +68,9 @@ type InternalQueryResult = Omit< >; function noop() {} -export const lastWatchOptions = Symbol(); +const lastWatchOptions = Symbol(); -export interface ObsQueryWithMeta +interface ObsQueryWithMeta extends ObservableQuery { [lastWatchOptions]?: WatchQueryOptions; } diff --git a/src/react/internal/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts index 2e8ec0b276b..a719a83a232 100644 --- a/src/react/internal/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -29,8 +29,10 @@ type FetchMoreOptions = Parameters< ObservableQuery["fetchMore"] >[0]; -const QUERY_REFERENCE_SYMBOL: unique symbol = Symbol(); -const PROMISE_SYMBOL: unique symbol = Symbol(); +const QUERY_REFERENCE_SYMBOL: unique symbol = Symbol.for( + "apollo.internal.queryRef" +); +const PROMISE_SYMBOL: unique symbol = Symbol.for("apollo.internal.refPromise"); declare const QUERY_REF_BRAND: unique symbol; /** * A `QueryReference` is an opaque object returned by `useBackgroundQuery`. From 50f15a3625a33ab2cb2645b0540cf0778d8417de Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 6 Mar 2025 19:16:14 +0100 Subject: [PATCH 42/68] Version Packages (#12405) Co-authored-by: github-actions[bot] --- .changeset/grumpy-dogs-add.md | 5 ----- .changeset/grumpy-donuts-smoke.md | 5 ----- .changeset/rich-adults-destroy.md | 5 ----- CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 13 insertions(+), 18 deletions(-) delete mode 100644 .changeset/grumpy-dogs-add.md delete mode 100644 .changeset/grumpy-donuts-smoke.md delete mode 100644 .changeset/rich-adults-destroy.md diff --git a/.changeset/grumpy-dogs-add.md b/.changeset/grumpy-dogs-add.md deleted file mode 100644 index 227311db800..00000000000 --- a/.changeset/grumpy-dogs-add.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -To mitigate problems when Apollo Client ends up more than once in the bundle, some unique symbols were converted into `Symbol.for` calls. diff --git a/.changeset/grumpy-donuts-smoke.md b/.changeset/grumpy-donuts-smoke.md deleted file mode 100644 index d6bcb73a182..00000000000 --- a/.changeset/grumpy-donuts-smoke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fixes an issue where the DeepOmit type would turn optional properties into required properties. This should only affect you if you were using the omitDeep or stripTypename utilities exported by Apollo Client. diff --git a/.changeset/rich-adults-destroy.md b/.changeset/rich-adults-destroy.md deleted file mode 100644 index 51ca5fac23f..00000000000 --- a/.changeset/rich-adults-destroy.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Show `NaN` rather than converting to `null` in debug messages from `MockLink` for unmatched `variables` values. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a7e6f9e7ca..e3e92b79cbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # @apollo/client +## 3.13.2 + +### Patch Changes + +- [#12409](https://github.com/apollographql/apollo-client/pull/12409) [`6aa2f3e`](https://github.com/apollographql/apollo-client/commit/6aa2f3e81ee0ae59da7ae0b12000bb5a55ec5c6d) Thanks [@phryneas](https://github.com/phryneas)! - To mitigate problems when Apollo Client ends up more than once in the bundle, some unique symbols were converted into `Symbol.for` calls. + +- [#12392](https://github.com/apollographql/apollo-client/pull/12392) [`644bb26`](https://github.com/apollographql/apollo-client/commit/644bb2662168a9bac0519be6979f0db38b0febc4) Thanks [@Joja81](https://github.com/Joja81)! - Fixes an issue where the DeepOmit type would turn optional properties into required properties. This should only affect you if you were using the omitDeep or stripTypename utilities exported by Apollo Client. + +- [#12404](https://github.com/apollographql/apollo-client/pull/12404) [`4332b88`](https://github.com/apollographql/apollo-client/commit/4332b886f0409145af57f26d334f86e5a1b465c5) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Show `NaN` rather than converting to `null` in debug messages from `MockLink` for unmatched `variables` values. + ## 3.13.1 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 7e468e67dd0..aafe1c69cad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.13.1", + "version": "3.13.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.13.1", + "version": "3.13.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e265d33c34a..8a3c028a2f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.13.1", + "version": "3.13.2", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From f6d387c166cc76f08135966fb6d74fd8fe808c21 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 7 Mar 2025 10:04:27 -0700 Subject: [PATCH 43/68] Fix `networkStatus` incorrectly reported as `ready` when using `errorPolicy: 'all'` with errors (#12362) --- .changeset/khaki-cheetahs-lick.md | 7 +++++++ .size-limits.json | 4 ++-- src/core/ObservableQuery.ts | 11 +++++++++++ src/core/__tests__/ObservableQuery.ts | 12 ++++++++---- src/react/hooks/__tests__/useLazyQuery.test.tsx | 2 ++ src/react/hooks/__tests__/useQuery.test.tsx | 10 +++++----- 6 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 .changeset/khaki-cheetahs-lick.md diff --git a/.changeset/khaki-cheetahs-lick.md b/.changeset/khaki-cheetahs-lick.md new file mode 100644 index 00000000000..8b8e8ae78ad --- /dev/null +++ b/.changeset/khaki-cheetahs-lick.md @@ -0,0 +1,7 @@ +--- +"@apollo/client": patch +--- + +Fixes an issue where calling `observableQuery.getCurrentResult()` when the `errorPolicy` was set to `all` would return the `networkStatus` as `NetworkStatus.ready` when there were errors returned in the result. This has been corrected to report `NetworkStatus.error`. + +This bug also affected the `useQuery` and `useLazyQuery` hooks and may affect you if you check for `networkStatus` in your component. diff --git a/.size-limits.json b/.size-limits.json index ebe7da3770a..f578ddfd656 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42243, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34437 + "dist/apollo-client.min.cjs": 42260, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34450 } diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 2f3e5c785de..21217aa1552 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -303,6 +303,17 @@ export class ObservableQuery< result.partial = true; } + // We need to check for both both `error` and `errors` field because there + // are cases where sometimes `error` is set, but not `errors` and + // vice-versa. This will be updated in the next major version when + // `errors` is deprecated in favor of `error`. + if ( + result.networkStatus === NetworkStatus.ready && + (result.error || result.errors) + ) { + result.networkStatus = NetworkStatus.error; + } + if ( __DEV__ && !diff.complete && diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index f367b11cb55..f7ab87bc41c 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -1003,7 +1003,7 @@ describe("ObservableQuery", () => { data: undefined, errors: [error], loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, // TODO: This is not present on the emitted result so this should match partial: true, }); @@ -2279,6 +2279,7 @@ describe("ObservableQuery", () => { const result = await observable.result(); const currentResult = observable.getCurrentResult(); + // TODO: This should include an `error` property, not just `errors` expect(result).toEqualApolloQueryResult({ data: dataOne, errors: [error], @@ -2289,9 +2290,7 @@ describe("ObservableQuery", () => { data: dataOne, errors: [error], loading: false, - // TODO: The networkStatus returned here is different than the one - // returned from `observable.result()`. These should match - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, }); }); @@ -2497,6 +2496,11 @@ describe("ObservableQuery", () => { loading: false, networkStatus: NetworkStatus.ready, }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx index a148a8256bc..c9e57c608b0 100644 --- a/src/react/hooks/__tests__/useLazyQuery.test.tsx +++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx @@ -1533,6 +1533,8 @@ describe("useLazyQuery Hook", () => { variables: {}, }); } + + await expect(takeSnapshot).not.toRerender(); }); it("the promise should not cause an unhandled rejection", async () => { diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index e71288020c9..8ed54a070a7 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -3971,7 +3971,7 @@ describe("useQuery Hook", () => { errors: [{ message: "error" }], called: true, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, previousData: undefined, variables: {}, }); @@ -4039,7 +4039,7 @@ describe("useQuery Hook", () => { errors: [{ message: 'Could not fetch "hello"' }], called: true, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, previousData: undefined, variables: {}, }); @@ -4103,7 +4103,7 @@ describe("useQuery Hook", () => { errors: [{ message: 'Could not fetch "hello"' }], called: true, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, previousData: undefined, variables: {}, }); @@ -12025,7 +12025,7 @@ describe("useQuery Hook", () => { ], called: true, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, previousData: { hero: { heroFriends: [ @@ -13506,7 +13506,7 @@ describe("useQuery Hook", () => { errors: [{ message: "Couldn't get name" }], called: true, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, previousData: undefined, variables: {}, }); From cb0ca9a0cf4035fc3f0a1b888e5289751de2b440 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 7 Mar 2025 10:10:42 -0700 Subject: [PATCH 44/68] Version Packages (#12417) Co-authored-by: github-actions[bot] --- .changeset/khaki-cheetahs-lick.md | 7 ------- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) delete mode 100644 .changeset/khaki-cheetahs-lick.md diff --git a/.changeset/khaki-cheetahs-lick.md b/.changeset/khaki-cheetahs-lick.md deleted file mode 100644 index 8b8e8ae78ad..00000000000 --- a/.changeset/khaki-cheetahs-lick.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fixes an issue where calling `observableQuery.getCurrentResult()` when the `errorPolicy` was set to `all` would return the `networkStatus` as `NetworkStatus.ready` when there were errors returned in the result. This has been corrected to report `NetworkStatus.error`. - -This bug also affected the `useQuery` and `useLazyQuery` hooks and may affect you if you check for `networkStatus` in your component. diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e92b79cbe..c831af5d560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # @apollo/client +## 3.13.3 + +### Patch Changes + +- [#12362](https://github.com/apollographql/apollo-client/pull/12362) [`f6d387c`](https://github.com/apollographql/apollo-client/commit/f6d387c166cc76f08135966fb6d74fd8fe808c21) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fixes an issue where calling `observableQuery.getCurrentResult()` when the `errorPolicy` was set to `all` would return the `networkStatus` as `NetworkStatus.ready` when there were errors returned in the result. This has been corrected to report `NetworkStatus.error`. + + This bug also affected the `useQuery` and `useLazyQuery` hooks and may affect you if you check for `networkStatus` in your component. + ## 3.13.2 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index aafe1c69cad..8338fcb7648 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.13.2", + "version": "3.13.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.13.2", + "version": "3.13.3", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 8a3c028a2f2..42ff6284708 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.13.2", + "version": "3.13.3", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From fee9368750e242ea03dea8d1557683506d411d8d Mon Sep 17 00:00:00 2001 From: Joren Broekema Date: Mon, 10 Mar 2025 17:25:40 +0100 Subject: [PATCH 45/68] fix: use import star from rehackt CJS dep (#12420) --- .changeset/quiet-rockets-judge.md | 5 +++++ src/react/hooks/internal/__tests__/useRenderGuard.test.tsx | 4 ++-- src/react/hooks/useSuspenseFragment.ts | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 .changeset/quiet-rockets-judge.md diff --git a/.changeset/quiet-rockets-judge.md b/.changeset/quiet-rockets-judge.md new file mode 100644 index 00000000000..f7854eaa87e --- /dev/null +++ b/.changeset/quiet-rockets-judge.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Use import star from `rehackt` to prevent issues with importing named exports from external CJS modules. diff --git a/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx b/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx index 0f60cb58892..8774e09b5f0 100644 --- a/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx +++ b/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx @@ -1,5 +1,5 @@ /* eslint-disable testing-library/render-result-naming-convention */ -import React, { useEffect } from "rehackt"; +import * as React from "rehackt"; import { useRenderGuard } from "../useRenderGuard"; import { render, waitFor } from "@testing-library/react"; @@ -23,7 +23,7 @@ it("returns a function that returns `false` if called after render", async () => let result: boolean | typeof UNDEF = UNDEF; function TestComponent() { const calledDuringRender = useRenderGuard(); - useEffect(() => { + React.useEffect(() => { result = calledDuringRender(); }); return <>Test; diff --git a/src/react/hooks/useSuspenseFragment.ts b/src/react/hooks/useSuspenseFragment.ts index 646600b8c13..5aee9e23ac1 100644 --- a/src/react/hooks/useSuspenseFragment.ts +++ b/src/react/hooks/useSuspenseFragment.ts @@ -9,7 +9,7 @@ import type { import { canonicalStringify } from "../../cache/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { getSuspenseCache } from "../internal/index.js"; -import React, { useMemo } from "rehackt"; +import * as React from "rehackt"; import type { FragmentKey } from "../internal/cache/types.js"; import { __use } from "./internal/__use.js"; import { wrapHook } from "./internal/index.js"; @@ -120,7 +120,7 @@ function useSuspenseFragment_< const { from, variables } = options; const { cache } = client; - const id = useMemo( + const id = React.useMemo( () => typeof from === "string" ? from : from === null ? null From 0d4b09e0d67547af41bf0d9cbb653cebf34b8dec Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 11:04:49 -0600 Subject: [PATCH 46/68] Version Packages (#12424) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/quiet-rockets-judge.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/quiet-rockets-judge.md diff --git a/.changeset/quiet-rockets-judge.md b/.changeset/quiet-rockets-judge.md deleted file mode 100644 index f7854eaa87e..00000000000 --- a/.changeset/quiet-rockets-judge.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Use import star from `rehackt` to prevent issues with importing named exports from external CJS modules. diff --git a/CHANGELOG.md b/CHANGELOG.md index c831af5d560..4d389ff185f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/client +## 3.13.4 + +### Patch Changes + +- [#12420](https://github.com/apollographql/apollo-client/pull/12420) [`fee9368`](https://github.com/apollographql/apollo-client/commit/fee9368750e242ea03dea8d1557683506d411d8d) Thanks [@jorenbroekema](https://github.com/jorenbroekema)! - Use import star from `rehackt` to prevent issues with importing named exports from external CJS modules. + ## 3.13.3 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 8338fcb7648..69f53f92aac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.13.3", + "version": "3.13.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.13.3", + "version": "3.13.4", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 42ff6284708..996a6814e7c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.13.3", + "version": "3.13.4", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From f3b5d2995a4dfd267b04b82c5cdf6e2d51380654 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 11 Mar 2025 14:15:30 +0100 Subject: [PATCH 47/68] rename size-limit names on `main` (#12426) --- .size-limit.cjs | 29 +++++++++++++++++++++++++++++ .size-limits.json | 4 ++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/.size-limit.cjs b/.size-limit.cjs index a91cfa60a04..23a23080afe 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,5 +1,33 @@ const limits = require("./.size-limits.json"); +/* prettier-ignore */ +const nameMapping = { + 'import { ApolloClient, InMemoryCache, HttpLink } from "dist/main.cjs"': 'import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (CJS)', + 'import { ApolloClient, InMemoryCache, HttpLink } from "dist/main.cjs" (production)': 'import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (production) (CJS)', + 'import { ApolloClient, InMemoryCache, HttpLink } from "dist/index.js"': 'import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client"', + 'import { ApolloClient, InMemoryCache, HttpLink } from "dist/index.js" (production)': 'import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (production)', + 'import { ApolloProvider } from "dist/react/index.js"': 'import { ApolloProvider } from "@apollo/client/react"', + 'import { ApolloProvider } from "dist/react/index.js" (production)': 'import { ApolloProvider } from "@apollo/client/react" (production)', + 'import { useQuery } from "dist/react/index.js"': 'import { useQuery } from "@apollo/client/react"', + 'import { useQuery } from "dist/react/index.js" (production)': 'import { useQuery } from "@apollo/client/react" (production)', + 'import { useLazyQuery } from "dist/react/index.js"': 'import { useLazyQuery } from "@apollo/client/react"', + 'import { useLazyQuery } from "dist/react/index.js" (production)': 'import { useLazyQuery } from "@apollo/client/react"', + 'import { useMutation } from "dist/react/index.js"': 'import { useMutation } from "@apollo/client/react"', + 'import { useMutation } from "dist/react/index.js" (production)': 'import { useMutation } from "@apollo/client/react" (production)', + 'import { useSubscription } from "dist/react/index.js"': 'import { useSubscription } from "@apollo/client/react"', + 'import { useSubscription } from "dist/react/index.js" (production)': 'import { useSubscription } from "@apollo/client/react" (production)', + 'import { useSuspenseQuery } from "dist/react/index.js"': 'import { useSuspenseQuery } from "@apollo/client/react"', + 'import { useSuspenseQuery } from "dist/react/index.js" (production)': 'import { useSuspenseQuery } from "@apollo/client/react" (production)', + 'import { useBackgroundQuery } from "dist/react/index.js"': 'import { useBackgroundQuery } from "@apollo/client/react"', + 'import { useBackgroundQuery } from "dist/react/index.js" (production)': 'import { useBackgroundQuery } from "@apollo/client/react" (production)', + 'import { useLoadableQuery } from "dist/react/index.js"': 'import { useLoadableQuery } from "@apollo/client/react"', + 'import { useLoadableQuery } from "dist/react/index.js" (production)': 'import { useLoadableQuery } from "@apollo/client/react" (production)', + 'import { useReadQuery } from "dist/react/index.js"': 'import { useReadQuery } from "@apollo/client/react"', + 'import { useReadQuery } from "dist/react/index.js" (production)': 'import { useReadQuery } from "@apollo/client/react" (production)', + 'import { useFragment } from "dist/react/index.js"': 'import { useFragment } from "@apollo/client/react"', + 'import { useFragment } from "dist/react/index.js" (production)': 'import { useFragment } from "@apollo/client/react" (production)', +}; + const checks = [ { path: "dist/apollo-client.min.cjs", @@ -73,6 +101,7 @@ const checks = [ ] ) .map((value) => { + value.name = nameMapping[value.name] || value.name; value.limit = limits[value.name]; return value; }); diff --git a/.size-limits.json b/.size-limits.json index f578ddfd656..59b05c94ee5 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42260, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34450 + "dist/apollo-client.min.cjs": 42244, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34450 } From c53d9687383b443627d2ee1d218ff424fe11bc36 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 12 Mar 2025 09:18:20 -0600 Subject: [PATCH 48/68] Update ROADMAP.md --- ROADMAP.md | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 84c080c3bf0..b0ff426da73 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 🔮 Apollo Client Ecosystem Roadmap -**Last updated: 2025-02-12** +**Last updated: 2025-03-12** For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md). @@ -17,20 +17,14 @@ For up to date release notes, refer to the project's [Changelog](https://github. ### Apollo Client -#### 3.13.0 - February 13, 2024 -_Release candidate - February 6th_ +#### 3.14.0 - TBD +_Release candidate - TBD -- `useSuspenseFragment` -- `onCompleted` and `onError` callback deprecations +- 4.0 compatibility release/deprecations #### [4.0.0](https://github.com/apollographql/apollo-client/milestone/31) - TBD _Release candidate - Mid April 2025_ -#### Upcoming changes - -##### 3.14.0 -- Deprecations and preparations for 4.0 - ### GraphQL Testing Library - New documentation @@ -56,6 +50,8 @@ _These changes will take longer than anticipated due to prioritization on Apollo - New/more robust documentation - Support for `@defer` with `PreloadQuery` (merged) -- Support for Apollo Client Streaming in TanStack Router +- Support for Apollo Client Streaming in TanStack Router (merged) - Support for Apollo Client Streaming in React Router 7 (merged) -- Remove `experimental` label from Next.js integration +- Remove `experimental` label from Next.js integration (merged) + +Try out the [alpha](https://github.com/apollographql/apollo-client-nextjs/blob/next/README.md) today and give us feedback! From bd1cb27dfec8182900879d46de61a90a3c6a9ace Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:05:19 -0600 Subject: [PATCH 49/68] chore(deps): update cimg/node docker tag to v23.9.0 (#12380) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .circleci/config.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 57f5c735e67..02a012a0be2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -12,7 +12,7 @@ jobs: Lint: docker: - - image: cimg/node:23.7.0 + - image: cimg/node:23.9.0 steps: - checkout - run: npm version @@ -21,7 +21,7 @@ jobs: Formatting: docker: - - image: cimg/node:23.7.0 + - image: cimg/node:23.9.0 steps: - checkout - run: npm ci @@ -29,7 +29,7 @@ jobs: Tests: docker: - - image: cimg/node:23.7.0 + - image: cimg/node:23.9.0 parameters: project: type: string @@ -50,7 +50,7 @@ jobs: path: reports/junit Attest: docker: - - image: cimg/node:23.7.0 + - image: cimg/node:23.9.0 steps: - checkout - run: npm ci @@ -58,7 +58,7 @@ jobs: BuildTarball: docker: - - image: cimg/node:23.7.0 + - image: cimg/node:23.9.0 steps: - checkout - run: npm run ci:precheck @@ -77,7 +77,7 @@ jobs: react: type: string docker: - - image: cimg/node:23.7.0-browsers + - image: cimg/node:23.9.0-browsers steps: - checkout - attach_workspace: @@ -115,7 +115,7 @@ jobs: externalPackage: type: string docker: - - image: cimg/node:23.7.0 + - image: cimg/node:23.9.0 steps: - checkout - attach_workspace: From e1ea1f593146dfeea575a61979edc022916f37bb Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 19 Mar 2025 15:56:20 -0600 Subject: [PATCH 50/68] Add defer to sidebar --- docs/source/_sidebar.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/_sidebar.yaml b/docs/source/_sidebar.yaml index d26de2ac14a..b30a00a5575 100644 --- a/docs/source/_sidebar.yaml +++ b/docs/source/_sidebar.yaml @@ -35,6 +35,8 @@ items: href: ./data/error-handling - label: Document transforms href: ./data/document-transforms + - label: Deferring response data + href: ./data/defer - label: Best practices href: ./data/operation-best-practices - label: Caching From 89b950ca752617a4935b019233dc67a48512aaec Mon Sep 17 00:00:00 2001 From: Maria Elisabeth Schreiber Date: Wed, 19 Mar 2025 15:56:49 -0600 Subject: [PATCH 51/68] Add minVersion --- docs/source/data/defer.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/source/data/defer.mdx b/docs/source/data/defer.mdx index 6b43736590b..eb364a450a5 100644 --- a/docs/source/data/defer.mdx +++ b/docs/source/data/defer.mdx @@ -1,6 +1,7 @@ --- title: "Using the @defer directive in Apollo Client" description: Receive query response data incrementally +minVersion: 3.7.0 --- > **The `@defer` directive is currently at the [General Availability stage](/resources/product-launch-stages/#general-availability) in Apollo Client, and is available by installing `@apollo/client@latest`.** If you have feedback on it, please let us know via [GitHub issues](https://github.com/apollographql/apollo-client/issues/new?assignees=&labels=&template=bug.md). From 12c8d06f1ef7cfbece8e3a63b7ad09d91334f663 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 20 Mar 2025 08:50:33 -0600 Subject: [PATCH 52/68] Fix an issue where incorrect results are returned when changing variables and skipping (#12461) --- .changeset/little-trainers-compare.md | 5 ++ .size-limits.json | 4 +- src/core/QueryInfo.ts | 16 ++-- src/react/hooks/__tests__/useQuery.test.tsx | 93 +++++++++++++++++++++ 4 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 .changeset/little-trainers-compare.md diff --git a/.changeset/little-trainers-compare.md b/.changeset/little-trainers-compare.md new file mode 100644 index 00000000000..e463d464519 --- /dev/null +++ b/.changeset/little-trainers-compare.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix an issue where a `cache-first` query would return the result for previous variables when a cache update is issued after simultaneously changing variables and skipping the query. diff --git a/.size-limits.json b/.size-limits.json index 59b05c94ee5..16a5b16c6f0 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42244, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34450 + "dist/apollo-client.min.cjs": 42263, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34469 } diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index 2c065972b78..7d8f892681b 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -86,6 +86,7 @@ export class QueryInfo { graphQLErrors?: ReadonlyArray; stopped = false; + private cancelWatch?: () => void; private cache: ApolloCache; constructor( @@ -128,6 +129,8 @@ export class QueryInfo { if (!equal(query.variables, this.variables)) { this.lastDiff = void 0; + // Ensure we don't continue to receive cache updates for old variables + this.cancel(); } Object.assign(this, { @@ -308,20 +311,17 @@ export class QueryInfo { // Cancel the pending notify timeout this.reset(); - this.cancel(); - // Revert back to the no-op version of cancel inherited from - // QueryInfo.prototype. - this.cancel = QueryInfo.prototype.cancel; const oq = this.observableQuery; if (oq) oq.stopPolling(); } } - // This method is a no-op by default, until/unless overridden by the - // updateWatch method. - private cancel() {} + private cancel() { + this.cancelWatch?.(); + this.cancelWatch = void 0; + } private lastWatch?: Cache.WatchOptions; @@ -342,7 +342,7 @@ export class QueryInfo { if (!this.lastWatch || !equal(watchOptions, this.lastWatch)) { this.cancel(); - this.cancel = this.cache.watch((this.lastWatch = watchOptions)); + this.cancelWatch = this.cache.watch((this.lastWatch = watchOptions)); } } diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 8ed54a070a7..06c22ef459c 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -1620,6 +1620,99 @@ describe("useQuery Hook", () => { await expect(takeSnapshot).not.toRerender(); }); + + // https://github.com/apollographql/apollo-client/issues/12458 + it("returns correct result when cache updates after changing variables and skipping query", async () => { + interface Data { + user: { + __typename: "User"; + id: number; + name: string; + }; + } + + const query: TypedDocumentNode = gql` + query ($id: ID!) { + user(id: $id) { + id + name + } + } + `; + + const client = new ApolloClient({ + link: ApolloLink.empty(), + cache: new InMemoryCache(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + ({ id, skip }) => useQuery(query, { skip, variables: { id } }), + { + initialProps: { id: 1, skip: true }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); + + client.writeQuery({ + query, + variables: { id: 1 }, + data: { user: { __typename: "User", id: 1, name: "User 1" } }, + }); + await rerender({ id: 1, skip: false }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { user: { __typename: "User", id: 1, name: "User 1" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); + + await rerender({ id: 2, skip: true }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { user: { __typename: "User", id: 1, name: "User 1" } }, + variables: { id: 2 }, + }); + + client.writeQuery({ + query, + variables: { id: 2 }, + data: { user: { __typename: "User", id: 2, name: "User 2" } }, + }); + + await rerender({ id: 2, skip: false }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { user: { __typename: "User", id: 2, name: "User 2" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { user: { __typename: "User", id: 1, name: "User 1" } }, + variables: { id: 2 }, + }); + + await expect(takeSnapshot).not.toRerender(); + }); }); describe("options.defaultOptions", () => { From d0cac27af95da5d7e0e8dcf70d8532750be1e5cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 20 Mar 2025 10:47:17 -0600 Subject: [PATCH 53/68] Version Packages (#12468) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/little-trainers-compare.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/little-trainers-compare.md diff --git a/.changeset/little-trainers-compare.md b/.changeset/little-trainers-compare.md deleted file mode 100644 index e463d464519..00000000000 --- a/.changeset/little-trainers-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fix an issue where a `cache-first` query would return the result for previous variables when a cache update is issued after simultaneously changing variables and skipping the query. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d389ff185f..14b8d38bc8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/client +## 3.13.5 + +### Patch Changes + +- [#12461](https://github.com/apollographql/apollo-client/pull/12461) [`12c8d06`](https://github.com/apollographql/apollo-client/commit/12c8d06f1ef7cfbece8e3a63b7ad09d91334f663) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix an issue where a `cache-first` query would return the result for previous variables when a cache update is issued after simultaneously changing variables and skipping the query. + ## 3.13.4 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 69f53f92aac..af08bd41b2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.13.4", + "version": "3.13.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.13.4", + "version": "3.13.5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 996a6814e7c..77343048e48 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.13.4", + "version": "3.13.5", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 820f6e656377fbc4f7d870976fe3563d32ec1ec1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 26 Mar 2025 09:40:15 -0600 Subject: [PATCH 54/68] Update ROADMAP.md --- ROADMAP.md | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index b0ff426da73..128e61f9813 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -17,13 +17,13 @@ For up to date release notes, refer to the project's [Changelog](https://github. ### Apollo Client -#### 3.14.0 - TBD -_Release candidate - TBD +#### 3.14.0 - May 13th, 2025 +_Release candidate - May 8th, 2025 - 4.0 compatibility release/deprecations -#### [4.0.0](https://github.com/apollographql/apollo-client/milestone/31) - TBD -_Release candidate - Mid April 2025_ +#### [4.0.0](https://github.com/apollographql/apollo-client/milestone/31) - June 10th, 2025 +_Release candidate - May 13th 2025_ ### GraphQL Testing Library @@ -50,8 +50,14 @@ _These changes will take longer than anticipated due to prioritization on Apollo - New/more robust documentation - Support for `@defer` with `PreloadQuery` (merged) -- Support for Apollo Client Streaming in TanStack Router (merged) -- Support for Apollo Client Streaming in React Router 7 (merged) -- Remove `experimental` label from Next.js integration (merged) + +**Next.js** +- Remove `experimental` label from Next.js integration (merged) - Public release March 31st, 2025 + +**TanStack Start** +- Support for Apollo Client Streaming in TanStack Router - will stay alpha + +**React Router** +- Support for Apollo Client Streaming in React Router 7 - will stay alpha Try out the [alpha](https://github.com/apollographql/apollo-client-nextjs/blob/next/README.md) today and give us feedback! From cdc55ff54bf4c83ec8571508ec4bf8156af1bc97 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 4 Apr 2025 17:37:17 +0200 Subject: [PATCH 55/68] keep `ObservableQuery` initiated from `useQuery` inactive before first subscription (#12285) * experiment: keep ObservableQuery initiated from `useQuery` inactive before first subscription * changeset * different handling during SSR * adjust test * use a slot instead of a config option * chores * change to `private` property * remove unnecessary condition * appease linter * rename `getQuery` to `getOrCreateQuery`, call `this.queries.get` where not needed * adjust test * chores * remove unused import * Update src/react/hoc/__tests__/queries/lifecycle.test.tsx Co-authored-by: Jerel Miller * Update src/core/__tests__/ApolloClient/general.test.ts Co-authored-by: Jerel Miller --------- Co-authored-by: Jerel Miller --- .api-reports/api-report-core.api.md | 8 ++-- .api-reports/api-report-react.api.md | 8 ++-- .../api-report-react_components.api.md | 8 ++-- .api-reports/api-report-react_context.api.md | 8 ++-- .api-reports/api-report-react_hoc.api.md | 8 ++-- .api-reports/api-report-react_hooks.api.md | 8 ++-- .api-reports/api-report-react_internal.api.md | 8 ++-- .api-reports/api-report-react_ssr.api.md | 8 ++-- .api-reports/api-report-testing.api.md | 8 ++-- .api-reports/api-report-testing_core.api.md | 8 ++-- .api-reports/api-report-utilities.api.md | 8 ++-- .api-reports/api-report.api.md | 8 ++-- .changeset/lemon-pigs-listen.md | 5 +++ .size-limits.json | 4 +- src/core/ObservableQuery.ts | 21 +++++++++- src/core/QueryManager.ts | 34 +++++++-------- .../__tests__/ApolloClient/general.test.ts | 6 +-- .../hoc/__tests__/queries/lifecycle.test.tsx | 41 +++++++++++-------- src/react/hooks/__tests__/useQuery.test.tsx | 2 +- src/react/hooks/useQuery.ts | 17 ++++---- 20 files changed, 126 insertions(+), 100 deletions(-) create mode 100644 .changeset/lemon-pigs-listen.md diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 017e6932435..1b8055f49cc 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1944,6 +1944,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2008,8 +2010,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2533,8 +2533,8 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:162: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:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" 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.api.md b/.api-reports/api-report-react.api.md index b2aae34e238..68538be587a 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1717,6 +1717,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1784,8 +1786,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2589,8 +2589,8 @@ interface WatchQueryOptions { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1597,8 +1599,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2039,8 +2039,8 @@ interface WatchQueryOptions { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1524,8 +1526,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1958,8 +1958,8 @@ interface WatchQueryOptions { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1565,8 +1567,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1969,8 +1969,8 @@ export function withSubscription { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1655,8 +1657,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2428,8 +2428,8 @@ interface WatchQueryOptions { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1756,8 +1758,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2540,8 +2540,8 @@ export function wrapQueryRef(inter // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105: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:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:175: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-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index e02d9d2905d..a774e8e3f3a 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1442,6 +1442,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1509,8 +1511,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1943,8 +1943,8 @@ interface WatchQueryOptions { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1587,8 +1589,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2002,8 +2002,8 @@ export function withWarningSpy(it: (...args: TArgs // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105: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:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:175: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.api.md b/.api-reports/api-report-testing_core.api.md index fa5b233b320..662f3a2ecdb 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1477,6 +1477,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1544,8 +1546,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1959,8 +1959,8 @@ export function withWarningSpy(it: (...args: TArgs // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105: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:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:175: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.api.md b/.api-reports/api-report-utilities.api.md index 33d2d83be42..b4f2c2d1eef 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -2252,6 +2252,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2319,8 +2321,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2904,8 +2904,8 @@ 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:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:175: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.api.md b/.api-reports/api-report.api.md index cfb416b8fc8..0826669245e 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2297,6 +2297,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2361,8 +2363,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -3279,8 +3279,8 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:162: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:118:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:119:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" 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/.changeset/lemon-pigs-listen.md b/.changeset/lemon-pigs-listen.md new file mode 100644 index 00000000000..9f9cd36f8a2 --- /dev/null +++ b/.changeset/lemon-pigs-listen.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +keep ObservableQuery created by useQuery non-active before it is first subscribed diff --git a/.size-limits.json b/.size-limits.json index 16a5b16c6f0..3be3557f7d4 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42263, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34469 + "dist/apollo-client.min.cjs": 42354, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34547 } diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 21217aa1552..0573af5d0e9 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -40,6 +40,7 @@ import type { MissingTree } from "../cache/core/types/common.js"; import { equalByQuery } from "./equalByQuery.js"; import type { TODO } from "../utilities/types/TODO.js"; import type { MaybeMasked, Unmasked } from "../masking/index.js"; +import { Slot } from "optimism"; const { assign, hasOwnProperty } = Object; @@ -66,6 +67,15 @@ export class ObservableQuery< TData = any, TVariables extends OperationVariables = OperationVariables, > extends Observable>> { + /** + * @internal + * A slot used by the `useQuery` hook to indicate that `client.watchQuery` + * should not register the query immediately, but instead wait for the query to + * be started registered with the `QueryManager` when `useSyncExternalStore` + * actively subscribes to it. + */ + private static inactiveOnCreation = new Slot(); + public readonly options: WatchQueryOptions; public readonly queryId: string; public readonly queryName?: string; @@ -119,7 +129,13 @@ export class ObservableQuery< queryInfo: QueryInfo; options: WatchQueryOptions; }) { + let startedInactive = ObservableQuery.inactiveOnCreation.getValue(); super((observer: Observer>>) => { + if (startedInactive) { + queryManager["queries"].set(this.queryId, queryInfo); + startedInactive = false; + } + // Zen Observable has its own error function, so in order to log correctly // we need to provide a custom error callback. try { @@ -828,9 +844,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, ) { // TODO Make sure we update the networkStatus (and infer fetchVariables) // before actually committing to the fetch. - this.queryManager.setObservableQuery(this); + const queryInfo = this.queryManager.getOrCreateQuery(this.queryId); + queryInfo.setObservableQuery(this); return this.queryManager["fetchConcastWithInfo"]( - this.queryId, + queryInfo, options, newNetworkStatus, query diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 5a32bad1cca..189cec239d8 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -658,8 +658,11 @@ export class QueryManager { options: WatchQueryOptions, networkStatus?: NetworkStatus ): Promise> { - return this.fetchConcastWithInfo(queryId, options, networkStatus).concast - .promise as TODO; + return this.fetchConcastWithInfo( + this.getOrCreateQuery(queryId), + options, + networkStatus + ).concast.promise as TODO; } public getQueryStore() { @@ -780,7 +783,9 @@ export class QueryManager { }); observable["lastQuery"] = query; - this.queries.set(observable.queryId, queryInfo); + if (!ObservableQuery["inactiveOnCreation"].getValue()) { + this.queries.set(observable.queryId, queryInfo); + } // We give queryInfo the transformed query to ensure the first cache diff // uses the transformed query instead of the raw query @@ -955,7 +960,7 @@ export class QueryManager { // pre-allocate a new query ID here, using a special prefix to enable // cleaning up these temporary queries later, after fetching. const queryId = makeUniqueId("legacyOneTimeQuery"); - const queryInfo = this.getQuery(queryId).init({ + const queryInfo = this.getOrCreateQuery(queryId).init({ document: options.query, variables: options.variables, }); @@ -1010,7 +1015,9 @@ export class QueryManager { ) { observableQueryPromises.push(observableQuery.refetch()); } - this.getQuery(queryId).setDiff(null); + (this.queries.get(queryId) || observableQuery["queryInfo"]).setDiff( + null + ); } ); @@ -1019,10 +1026,6 @@ export class QueryManager { return Promise.all(observableQueryPromises); } - public setObservableQuery(observableQuery: ObservableQuery) { - this.getQuery(observableQuery.queryId).setObservableQuery(observableQuery); - } - public startGraphQLSubscription( options: SubscriptionOptions ): Observable> { @@ -1118,7 +1121,7 @@ export class QueryManager { // The same queryId could have two rejection fns for two promises this.fetchCancelFns.delete(queryId); if (this.queries.has(queryId)) { - this.getQuery(queryId).stop(); + this.queries.get(queryId)?.stop(); this.queries.delete(queryId); } } @@ -1303,7 +1306,7 @@ export class QueryManager { } private fetchConcastWithInfo( - queryId: string, + queryInfo: QueryInfo, options: WatchQueryOptions, // The initial networkStatus for this fetch, most often // NetworkStatus.loading, but also possibly fetchMore, poll, refetch, @@ -1312,7 +1315,6 @@ export class QueryManager { query = options.query ): ConcastAndInfo { const variables = this.getVariables(query, options.variables) as TVars; - const queryInfo = this.getQuery(queryId); const defaults = this.defaultOptions.watchQuery; let { @@ -1365,8 +1367,8 @@ export class QueryManager { // This cancel function needs to be set before the concast is created, // in case concast creation synchronously cancels the request. - const cleanupCancelFn = () => this.fetchCancelFns.delete(queryId); - this.fetchCancelFns.set(queryId, (reason) => { + const cleanupCancelFn = () => this.fetchCancelFns.delete(queryInfo.queryId); + this.fetchCancelFns.set(queryInfo.queryId, (reason) => { cleanupCancelFn(); // This delay ensures the concast variable has been initialized. setTimeout(() => concast.cancel(reason)); @@ -1435,7 +1437,7 @@ export class QueryManager { this.getObservableQueries(include).forEach((oq, queryId) => { includedQueriesById.set(queryId, { oq, - lastDiff: this.getQuery(queryId).getDiff(), + lastDiff: (this.queries.get(queryId) || oq["queryInfo"]).getDiff(), }); }); } @@ -1788,7 +1790,7 @@ export class QueryManager { } } - private getQuery(queryId: string): QueryInfo { + public getOrCreateQuery(queryId: string): QueryInfo { if (queryId && !this.queries.has(queryId)) { this.queries.set(queryId, new QueryInfo(this, queryId)); } diff --git a/src/core/__tests__/ApolloClient/general.test.ts b/src/core/__tests__/ApolloClient/general.test.ts index 6c7814ca1df..26a749e6827 100644 --- a/src/core/__tests__/ApolloClient/general.test.ts +++ b/src/core/__tests__/ApolloClient/general.test.ts @@ -28,7 +28,6 @@ import { WatchQueryFetchPolicy, WatchQueryOptions, } from "../../watchQueryOptions"; -import { QueryManager } from "../../QueryManager"; import { ApolloError } from "../../../errors"; @@ -2869,16 +2868,13 @@ describe("ApolloClient", () => { const mocks = mockFetchQuery(queryManager); const queryId = "1"; - const getQuery: QueryManager["getQuery"] = ( - queryManager as any - ).getQuery.bind(queryManager); const stream = new ObservableStream(observable); await expect(stream).toEmitNext(); { - const query = getQuery(queryId); + const query = queryManager.getOrCreateQuery(queryId); const fqbpCalls = mocks.fetchQueryByPolicy.mock.calls; expect(query.lastRequestId).toEqual(1); diff --git a/src/react/hoc/__tests__/queries/lifecycle.test.tsx b/src/react/hoc/__tests__/queries/lifecycle.test.tsx index 76134ba081c..d7b597ae356 100644 --- a/src/react/hoc/__tests__/queries/lifecycle.test.tsx +++ b/src/react/hoc/__tests__/queries/lifecycle.test.tsx @@ -720,6 +720,9 @@ describe("[queries] lifecycle", () => { }); it("handles synchronous racecondition with prefilled data from the server", async () => { + using _act = disableActEnvironment(); + const renderStream = createRenderStream>(); + const query: DocumentNode = gql` query GetUser($first: Int) { user(first: $first) { @@ -754,36 +757,40 @@ describe("[queries] lifecycle", () => { cache: new Cache({ addTypename: false }).restore(initialState), }); - let count = 0; - let done = false; const Container = graphql(query)( class extends React.Component> { - componentDidMount() { - void this.props.data!.refetch().then((result) => { - expect(result.data!.user.name).toBe("Luke Skywalker"); - done = true; - }); - } - render() { - count++; - const user = this.props.data!.user; - const name = user ? user.name : ""; - if (count === 2) { - expect(name).toBe("Luke Skywalker"); - } + renderStream.replaceSnapshot(this.props); return null; } } ); - render( + await renderStream.render( ); + { + await renderStream.takeRender(); + } + const done = renderStream + .getCurrentRender() + .snapshot.data!.refetch() + .then((result) => { + expect(result.data!.user.name).toBe("Luke Skywalker"); + }); + + { + const { snapshot } = await renderStream.takeRender(); + + const user = snapshot.data!.user; + const name = user ? user.name : ""; + + expect(name).toBe("Luke Skywalker"); + } - await waitFor(() => expect(done).toBeTruthy()); + await done; }); it("handles asynchronous racecondition with prefilled data from the server", async () => { diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 06c22ef459c..c80b89a45eb 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -2328,7 +2328,7 @@ describe("useQuery Hook", () => { function checkObservableQueries(expectedLinkCount: number) { const obsQueries = client.getObservableQueries("all"); const { observable } = getCurrentSnapshot(); - expect(obsQueries.size).toBe(2); + expect(obsQueries.size).toBe(IS_REACT_17 || IS_REACT_18 ? 2 : 1); const activeSet = new Set(); const inactiveSet = new Set(); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 7e81ce8feba..55d1637dbc3 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -23,23 +23,20 @@ import * as React from "rehackt"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import { equal } from "@wry/equality"; +import { mergeOptions } from "../../utilities/index.js"; +import { getApolloContext } from "../context/index.js"; +import { ApolloError } from "../../errors/index.js"; import type { ApolloClient, DefaultOptions, OperationVariables, WatchQueryFetchPolicy, -} from "../../core/index.js"; -import { mergeOptions } from "../../utilities/index.js"; -import { getApolloContext } from "../context/index.js"; -import { ApolloError } from "../../errors/index.js"; -import type { ApolloQueryResult, - ObservableQuery, DocumentNode, TypedDocumentNode, WatchQueryOptions, } from "../../core/index.js"; -import { NetworkStatus } from "../../core/index.js"; +import { ObservableQuery, NetworkStatus } from "../../core/index.js"; import type { QueryHookOptions, QueryResult, @@ -190,8 +187,10 @@ function useInternalState< // to fetch the result set. This is used during SSR. (renderPromises && renderPromises.getSSRObservable(makeWatchQueryOptions())) || - client.watchQuery( - getObsQueryOptions(void 0, client, options, makeWatchQueryOptions()) + ObservableQuery["inactiveOnCreation"].withValue(!renderPromises, () => + client.watchQuery( + getObsQueryOptions(void 0, client, options, makeWatchQueryOptions()) + ) ), resultData: { // Reuse previousData from previous InternalState (if any) to provide From 949bd144e6989c8d11b990d88bceb4be2a8e19da Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Apr 2025 18:12:24 +0200 Subject: [PATCH 56/68] Version Packages (#12527) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/lemon-pigs-listen.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/lemon-pigs-listen.md diff --git a/.changeset/lemon-pigs-listen.md b/.changeset/lemon-pigs-listen.md deleted file mode 100644 index 9f9cd36f8a2..00000000000 --- a/.changeset/lemon-pigs-listen.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -keep ObservableQuery created by useQuery non-active before it is first subscribed diff --git a/CHANGELOG.md b/CHANGELOG.md index 14b8d38bc8f..fdff62b2429 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/client +## 3.13.6 + +### Patch Changes + +- [#12285](https://github.com/apollographql/apollo-client/pull/12285) [`cdc55ff`](https://github.com/apollographql/apollo-client/commit/cdc55ff54bf4c83ec8571508ec4bf8156af1bc97) Thanks [@phryneas](https://github.com/phryneas)! - keep ObservableQuery created by useQuery non-active before it is first subscribed + ## 3.13.5 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index af08bd41b2f..087cb75aa85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.13.5", + "version": "3.13.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.13.5", + "version": "3.13.6", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 77343048e48..ab59302df57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.13.5", + "version": "3.13.6", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 009893220934081f6e5733bff5863c768a597117 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 10 Apr 2025 12:41:40 +0200 Subject: [PATCH 57/68] refactor some code in `QueryInfo` (#12540) * refactor: do away with `QueryInfo.listeners` * remove incorrect `info.reset` call, call `cache.diff` directly * remove obsolete `QueryListener` type * remove obsolete import * inline `shouldNotify` * move `cancelNotifyTimeout` onto `QueryInfo`, move methods together, create `scheduleNotify` method * move notify-related code from `QueryInfo` to `ObservableQuery` * move `reobserveCacheFirst` into a private method * changesets * chores, method visibility * size limits * type adjustment * PR feedback --- .api-reports/api-report-core.api.md | 15 +- .api-reports/api-report-react.api.md | 21 ++- .../api-report-react_components.api.md | 21 ++- .api-reports/api-report-react_context.api.md | 21 ++- .api-reports/api-report-react_hoc.api.md | 21 ++- .api-reports/api-report-react_hooks.api.md | 21 ++- .api-reports/api-report-react_internal.api.md | 21 ++- .api-reports/api-report-react_ssr.api.md | 21 ++- .api-reports/api-report-testing.api.md | 21 ++- .api-reports/api-report-testing_core.api.md | 21 ++- .../api-report-testing_experimental.api.md | 2 +- .api-reports/api-report-utilities.api.md | 21 ++- .api-reports/api-report.api.md | 15 +- .changeset/strange-wolves-fold.md | 5 + .changeset/weak-radios-trade.md | 5 + .size-limits.json | 4 +- src/core/ObservableQuery.ts | 135 +++++++++++++----- src/core/QueryInfo.ts | 92 +----------- src/core/QueryManager.ts | 6 +- src/core/__tests__/ObservableQuery.ts | 5 +- src/core/types.ts | 3 - 21 files changed, 214 insertions(+), 283 deletions(-) create mode 100644 .changeset/strange-wolves-fold.md create mode 100644 .changeset/weak-radios-trade.md diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 1b8055f49cc..3a5051ef34b 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1692,6 +1692,8 @@ export class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1711,6 +1713,8 @@ export class ObservableQuery>): ObservableSubscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1864,8 +1870,6 @@ class QueryInfo { // (undocumented) lastRequestId: number; // (undocumented) - listeners: Set; - // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) markReady(): NetworkStatus; @@ -1878,14 +1882,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1901,9 +1901,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -export type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 68538be587a..632a42dc1a9 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1401,6 +1401,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1420,6 +1422,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1626,10 +1632,6 @@ class QueryInfo { }): 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) @@ -1643,14 +1645,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1672,9 +1670,6 @@ export interface QueryLazyOptions { variables?: TVariables; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -2593,8 +2588,8 @@ interface WatchQueryOptions, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1291,6 +1293,8 @@ class ObservableQuery>): Subscription_2; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1445,10 +1451,6 @@ class QueryInfo { }): 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) @@ -1462,14 +1464,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1485,9 +1483,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -2043,8 +2038,8 @@ interface WatchQueryOptions, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1244,6 +1246,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1372,10 +1378,6 @@ class QueryInfo { }): 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) @@ -1389,14 +1391,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1412,9 +1410,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1962,8 +1957,8 @@ interface WatchQueryOptions, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1284,6 +1286,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1413,10 +1419,6 @@ class QueryInfo { }): 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) @@ -1430,14 +1432,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1453,9 +1451,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1973,8 +1968,8 @@ export function withSubscription, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1364,6 +1366,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1503,10 +1509,6 @@ class QueryInfo { }): 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) @@ -1520,14 +1522,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1543,9 +1541,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -2432,8 +2427,8 @@ interface WatchQueryOptions, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1399,6 +1401,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1598,10 +1604,6 @@ class QueryInfo { }): 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) @@ -1615,14 +1617,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1644,9 +1642,6 @@ export interface QueryKey { __queryKey?: string; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -2544,8 +2539,8 @@ export function wrapQueryRef(inter // src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/types.ts:172: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:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:51:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useBackgroundQuery.ts:75:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index a774e8e3f3a..b4650f350f3 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1210,6 +1210,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1229,6 +1231,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1357,10 +1363,6 @@ class QueryInfo { }): 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) @@ -1374,14 +1376,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1397,9 +1395,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1947,8 +1942,8 @@ interface WatchQueryOptions, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1360,6 +1362,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1437,10 +1443,6 @@ class QueryInfo { }): 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) @@ -1454,14 +1456,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1477,9 +1475,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -2006,8 +2001,8 @@ export function withWarningSpy(it: (...args: TArgs // src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/types.ts:172: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:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" 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.api.md b/.api-reports/api-report-testing_core.api.md index 662f3a2ecdb..7b577b57b93 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1296,6 +1296,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1315,6 +1317,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1392,10 +1398,6 @@ class QueryInfo { }): 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) @@ -1409,14 +1411,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1432,9 +1430,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1963,8 +1958,8 @@ export function withWarningSpy(it: (...args: TArgs // src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/types.ts:172: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:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" 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_experimental.api.md b/.api-reports/api-report-testing_experimental.api.md index cfbe5ed404e..101daec293c 100644 --- a/.api-reports/api-report-testing_experimental.api.md +++ b/.api-reports/api-report-testing_experimental.api.md @@ -79,7 +79,7 @@ interface TestSchemaOptions { // Warnings were encountered during analysis: // // 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/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" 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/testing/experimental/createTestSchema.ts:10:23 - (ae-forgotten-export) The symbol "Resolvers" 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.api.md b/.api-reports/api-report-utilities.api.md index b4f2c2d1eef..068367ef9a1 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -1987,6 +1987,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -2004,6 +2006,8 @@ class ObservableQuery>): ObservableSubscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -2167,10 +2173,6 @@ class QueryInfo { }): 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) @@ -2184,14 +2186,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -2207,9 +2205,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -2908,8 +2903,8 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/types.ts:172: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:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" 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 diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 0826669245e..79b576393fb 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -1911,6 +1911,8 @@ export class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1930,6 +1932,8 @@ export class ObservableQuery>): ObservableSubscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -2211,8 +2217,6 @@ class QueryInfo { // (undocumented) lastRequestId: number; // (undocumented) - listeners: Set; - // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) markReady(): NetworkStatus; @@ -2225,14 +2229,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -2254,9 +2254,6 @@ export interface QueryLazyOptions { variables?: TVariables; } -// @public (undocumented) -export type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts diff --git a/.changeset/strange-wolves-fold.md b/.changeset/strange-wolves-fold.md new file mode 100644 index 00000000000..afe3811db87 --- /dev/null +++ b/.changeset/strange-wolves-fold.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Refactor: Move notification scheduling logic from `QueryInfo` to `ObservableQuery` diff --git a/.changeset/weak-radios-trade.md b/.changeset/weak-radios-trade.md new file mode 100644 index 00000000000..b7fc99f136c --- /dev/null +++ b/.changeset/weak-radios-trade.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Refactored cache emit logic for ObservableQuery. This should be an invisible change. diff --git a/.size-limits.json b/.size-limits.json index 3be3557f7d4..ce52591ca48 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42354, - "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34547 + "dist/apollo-client.min.cjs": 42331, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34542 } diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 0573af5d0e9..889d4501c9b 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -627,7 +627,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, // the cache, we still want fetchMore to deliver its final loading:false // result with the unchanged data. if (isCached && !updatedQuerySet.has(this.query)) { - reobserveCacheFirst(this); + this.reobserveCacheFirst(); } }); } @@ -1175,51 +1175,110 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, } : result; } -} -// Necessary because the ObservableQuery constructor has a different -// signature than the Observable constructor. -fixObservableSubclass(ObservableQuery); + private dirty: boolean = false; -// Reobserve with fetchPolicy effectively set to "cache-first", triggering -// delivery of any new data from the cache, possibly falling back to the network -// if any cache data are missing. This allows _complete_ cache results to be -// delivered without also kicking off unnecessary network requests when -// this.options.fetchPolicy is "cache-and-network" or "network-only". When -// this.options.fetchPolicy is any other policy ("cache-first", "cache-only", -// "standby", or "no-cache"), we call this.reobserve() as usual. -export function reobserveCacheFirst( - obsQuery: ObservableQuery -) { - const { fetchPolicy, nextFetchPolicy } = obsQuery.options; - - if (fetchPolicy === "cache-and-network" || fetchPolicy === "network-only") { - return obsQuery.reobserve({ - fetchPolicy: "cache-first", - // Use a temporary nextFetchPolicy function that replaces itself with the - // previous nextFetchPolicy value and returns the original fetchPolicy. - nextFetchPolicy( - this: WatchQueryOptions, - currentFetchPolicy: WatchQueryFetchPolicy, - context: NextFetchPolicyContext + private notifyTimeout?: ReturnType; + + /** @internal */ + protected resetNotifications() { + this.cancelNotifyTimeout(); + this.dirty = false; + } + + private cancelNotifyTimeout() { + if (this.notifyTimeout) { + clearTimeout(this.notifyTimeout); + this.notifyTimeout = void 0; + } + } + + /** @internal */ + protected scheduleNotify() { + if (this.dirty) return; + this.dirty = true; + if (!this.notifyTimeout) { + this.notifyTimeout = setTimeout(() => this.notify(), 0); + } + } + + /** @internal */ + protected notify() { + this.cancelNotifyTimeout(); + + if (this.dirty) { + if ( + this.options.fetchPolicy == "cache-only" || + this.options.fetchPolicy == "cache-and-network" || + !isNetworkRequestInFlight(this.queryInfo.networkStatus) ) { - // Replace this nextFetchPolicy function in the options object with the - // original this.options.nextFetchPolicy value. - this.nextFetchPolicy = nextFetchPolicy; - // If the original nextFetchPolicy value was a function, give it a - // chance to decide what happens here. - if (typeof this.nextFetchPolicy === "function") { - return this.nextFetchPolicy(currentFetchPolicy, context); + const diff = this.queryInfo.getDiff(); + if (diff.fromOptimisticTransaction) { + // If this diff came from an optimistic transaction, deliver the + // current cache data to the ObservableQuery, but don't perform a + // reobservation, since oq.reobserveCacheFirst might make a network + // request, and we never want to trigger network requests in the + // middle of optimistic updates. + this.observe(); + } else { + // Otherwise, make the ObservableQuery "reobserve" the latest data + // using a temporary fetch policy of "cache-first", so complete cache + // results have a chance to be delivered without triggering additional + // network requests, even when options.fetchPolicy is "network-only" + // or "cache-and-network". All other fetch policies are preserved by + // this method, and are handled by calling oq.reobserve(). If this + // reobservation is spurious, isDifferentFromLastResult still has a + // chance to catch it before delivery to ObservableQuery subscribers. + this.reobserveCacheFirst(); } - // Otherwise go back to the original this.options.fetchPolicy. - return fetchPolicy!; - }, - }); + } + } + + this.dirty = false; } - return obsQuery.reobserve(); + // Reobserve with fetchPolicy effectively set to "cache-first", triggering + // delivery of any new data from the cache, possibly falling back to the network + // if any cache data are missing. This allows _complete_ cache results to be + // delivered without also kicking off unnecessary network requests when + // this.options.fetchPolicy is "cache-and-network" or "network-only". When + // this.options.fetchPolicy is any other policy ("cache-first", "cache-only", + // "standby", or "no-cache"), we call this.reobserve() as usual. + private reobserveCacheFirst() { + const { fetchPolicy, nextFetchPolicy } = this.options; + + if (fetchPolicy === "cache-and-network" || fetchPolicy === "network-only") { + return this.reobserve({ + fetchPolicy: "cache-first", + // Use a temporary nextFetchPolicy function that replaces itself with the + // previous nextFetchPolicy value and returns the original fetchPolicy. + nextFetchPolicy( + this: WatchQueryOptions, + currentFetchPolicy: WatchQueryFetchPolicy, + context: NextFetchPolicyContext + ) { + // Replace this nextFetchPolicy function in the options object with the + // original this.options.nextFetchPolicy value. + this.nextFetchPolicy = nextFetchPolicy; + // If the original nextFetchPolicy value was a function, give it a + // chance to decide what happens here. + if (typeof this.nextFetchPolicy === "function") { + return this.nextFetchPolicy(currentFetchPolicy, context); + } + // Otherwise go back to the original this.options.fetchPolicy. + return fetchPolicy!; + }, + }); + } + + return this.reobserve(); + } } +// Necessary because the ObservableQuery constructor has a different +// signature than the Observable constructor. +fixObservableSubclass(ObservableQuery); + function defaultSubscriptionObserverErrorCallback(error: ApolloError) { invariant.error("Unhandled error", error.message, error.stack); } diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index 7d8f892681b..2a228eb8d13 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -6,15 +6,13 @@ import { DeepMerger } from "../utilities/index.js"; 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 } from "./types.js"; import type { FetchResult } from "../link/core/index.js"; import { isNonEmptyArray, graphQLResultHasError, canUseWeakMap, } from "../utilities/index.js"; -import { NetworkStatus, isNetworkRequestInFlight } from "./networkStatus.js"; +import { NetworkStatus } from "./networkStatus.js"; import type { ApolloError } from "../errors/index.js"; import type { QueryManager } from "./QueryManager.js"; import type { Unmasked } from "../masking/index.js"; @@ -57,13 +55,6 @@ function wrapDestructiveCacheMethod( } } -function cancelNotifyTimeout(info: QueryInfo) { - if (info["notifyTimeout"]) { - clearTimeout(info["notifyTimeout"]); - info["notifyTimeout"] = void 0; - } -} - // A QueryInfo object represents a single query managed by the // QueryManager, which tracks all QueryInfo objects by queryId in its // this.queries Map. QueryInfo objects store the latest results and errors @@ -77,7 +68,6 @@ function cancelNotifyTimeout(info: QueryInfo) { // many public fields. The effort to lock down and simplify the QueryInfo // interface is ongoing, and further improvements are welcome. export class QueryInfo { - listeners = new Set(); document: DocumentNode | null = null; lastRequestId = 1; variables?: Record; @@ -152,15 +142,6 @@ export class QueryInfo { return this; } - private dirty: boolean = false; - - private notifyTimeout?: ReturnType; - - reset() { - cancelNotifyTimeout(this); - this.dirty = false; - } - resetDiff() { this.lastDiff = void 0; } @@ -230,79 +211,18 @@ export class QueryInfo { this.updateLastDiff(diff); - if (!this.dirty && !equal(oldDiff && oldDiff.result, diff && diff.result)) { - this.dirty = true; - if (!this.notifyTimeout) { - this.notifyTimeout = setTimeout(() => this.notify(), 0); - } + if (!equal(oldDiff && oldDiff.result, diff && diff.result)) { + this.observableQuery?.["scheduleNotify"](); } } public readonly observableQuery: ObservableQuery | null = null; - private oqListener?: QueryListener; - setObservableQuery(oq: ObservableQuery | null) { if (oq === this.observableQuery) return; - - if (this.oqListener) { - this.listeners.delete(this.oqListener); - } - (this as any).observableQuery = oq; - if (oq) { oq["queryInfo"] = this; - this.listeners.add( - (this.oqListener = () => { - const diff = this.getDiff(); - if (diff.fromOptimisticTransaction) { - // If this diff came from an optimistic transaction, deliver the - // current cache data to the ObservableQuery, but don't perform a - // reobservation, since oq.reobserveCacheFirst might make a network - // request, and we never want to trigger network requests in the - // middle of optimistic updates. - oq["observe"](); - } else { - // Otherwise, make the ObservableQuery "reobserve" the latest data - // using a temporary fetch policy of "cache-first", so complete cache - // results have a chance to be delivered without triggering additional - // network requests, even when options.fetchPolicy is "network-only" - // or "cache-and-network". All other fetch policies are preserved by - // this method, and are handled by calling oq.reobserve(). If this - // reobservation is spurious, isDifferentFromLastResult still has a - // chance to catch it before delivery to ObservableQuery subscribers. - reobserveCacheFirst(oq); - } - }) - ); - } else { - delete this.oqListener; - } - } - - notify() { - cancelNotifyTimeout(this); - - if (this.shouldNotify()) { - this.listeners.forEach((listener) => listener(this)); - } - - this.dirty = false; - } - - private shouldNotify() { - if (!this.dirty || !this.listeners.size) { - return false; } - - if (isNetworkRequestInFlight(this.networkStatus) && this.observableQuery) { - const { fetchPolicy } = this.observableQuery.options; - if (fetchPolicy !== "cache-only" && fetchPolicy !== "cache-and-network") { - return false; - } - } - - return true; } public stop() { @@ -310,7 +230,7 @@ export class QueryInfo { this.stopped = true; // Cancel the pending notify timeout - this.reset(); + this.observableQuery?.["resetNotifications"](); this.cancel(); const oq = this.observableQuery; @@ -387,7 +307,7 @@ export class QueryInfo { // Cancel the pending notify timeout (if it exists) to prevent extraneous network // requests. To allow future notify timeouts, diff and dirty are reset as well. - this.reset(); + this.observableQuery?.["resetNotifications"](); if ("incremental" in result && isNonEmptyArray(result.incremental)) { const mergedData = mergeIncrementalData(this.getDiff().result, result); @@ -513,7 +433,7 @@ export class QueryInfo { this.networkStatus = NetworkStatus.error; this.lastWrite = void 0; - this.reset(); + this.observableQuery?.["resetNotifications"](); if (error.graphQLErrors) { this.graphQLErrors = error.graphQLErrors; diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 189cec239d8..fbd58b36d5a 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -1128,7 +1128,7 @@ export class QueryManager { public broadcastQueries() { if (this.onBroadcast) this.onBroadcast(); - this.queries.forEach((info) => info.notify()); + this.queries.forEach((info) => info.observableQuery?.["notify"]()); } public getLocalState(): LocalState { @@ -1545,9 +1545,7 @@ export class QueryManager { // queries, even the QueryOptions ones. if (onQueryUpdated) { if (!diff) { - const info = oq["queryInfo"]; - info.reset(); // Force info.getDiff() to read from cache. - diff = info.getDiff(); + diff = this.cache.diff(oq["queryInfo"]["getDiffOptions"]()); } result = onQueryUpdated(oq, diff, lastDiff); } diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index f7ab87bc41c..21da5da9f7a 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -3443,7 +3443,10 @@ describe("ObservableQuery", () => { const queryInfo = observable["queryInfo"]; const cache = queryInfo["cache"]; const setDiffSpy = jest.spyOn(queryInfo, "setDiff"); - const notifySpy = jest.spyOn(queryInfo, "notify"); + const notifySpy = jest.spyOn( + observable, + "notify" as any /* this is not a public method so we cast */ + ); const stream = new ObservableStream(observable); diff --git a/src/core/types.ts b/src/core/types.ts index 434b9d1a098..65ba13776a1 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -3,7 +3,6 @@ import type { DocumentNode, GraphQLFormattedError } from "graphql"; import type { ApolloCache } from "../cache/index.js"; import type { FetchResult } from "../link/core/index.js"; import type { ApolloError } from "../errors/index.js"; -import type { QueryInfo } from "./QueryInfo.js"; import type { NetworkStatus } from "./networkStatus.js"; import type { Resolver } from "./LocalState.js"; import type { ObservableQuery } from "./ObservableQuery.js"; @@ -20,8 +19,6 @@ export type MethodKeys = { export interface DefaultContext extends Record {} -export type QueryListener = (queryInfo: QueryInfo) => void; - export type OnQueryUpdated = ( observableQuery: ObservableQuery, diff: Cache.DiffResult, From 616cb017b2890d87bec6a4f3f6c0c2406c3c471f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 10 Apr 2025 11:18:11 -0600 Subject: [PATCH 58/68] Version Packages (#12544) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/strange-wolves-fold.md | 5 ----- .changeset/weak-radios-trade.md | 5 ----- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- 5 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 .changeset/strange-wolves-fold.md delete mode 100644 .changeset/weak-radios-trade.md diff --git a/.changeset/strange-wolves-fold.md b/.changeset/strange-wolves-fold.md deleted file mode 100644 index afe3811db87..00000000000 --- a/.changeset/strange-wolves-fold.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Refactor: Move notification scheduling logic from `QueryInfo` to `ObservableQuery` diff --git a/.changeset/weak-radios-trade.md b/.changeset/weak-radios-trade.md deleted file mode 100644 index b7fc99f136c..00000000000 --- a/.changeset/weak-radios-trade.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Refactored cache emit logic for ObservableQuery. This should be an invisible change. diff --git a/CHANGELOG.md b/CHANGELOG.md index fdff62b2429..944442a64f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # @apollo/client +## 3.13.7 + +### Patch Changes + +- [#12540](https://github.com/apollographql/apollo-client/pull/12540) [`0098932`](https://github.com/apollographql/apollo-client/commit/009893220934081f6e5733bff5863c768a597117) Thanks [@phryneas](https://github.com/phryneas)! - Refactor: Move notification scheduling logic from `QueryInfo` to `ObservableQuery` + +- [#12540](https://github.com/apollographql/apollo-client/pull/12540) [`0098932`](https://github.com/apollographql/apollo-client/commit/009893220934081f6e5733bff5863c768a597117) Thanks [@phryneas](https://github.com/phryneas)! - Refactored cache emit logic for ObservableQuery. This should be an invisible change. + ## 3.13.6 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 087cb75aa85..44d57906de1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.13.6", + "version": "3.13.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.13.6", + "version": "3.13.7", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ab59302df57..983b3b657e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.13.6", + "version": "3.13.7", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From ef41a67476eeb325eacf9d04f2277014f4d4a419 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 11 Apr 2025 08:11:41 -0600 Subject: [PATCH 59/68] Update ROADMAP.md --- ROADMAP.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 128e61f9813..bebf56c3c93 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 🔮 Apollo Client Ecosystem Roadmap -**Last updated: 2025-03-12** +**Last updated: 2025-04-11** For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md). @@ -17,13 +17,13 @@ For up to date release notes, refer to the project's [Changelog](https://github. ### Apollo Client -#### 3.14.0 - May 13th, 2025 -_Release candidate - May 8th, 2025 +#### 3.14.0 - May 20th, 2025 +_Release candidate - May 15th, 2025 - 4.0 compatibility release/deprecations #### [4.0.0](https://github.com/apollographql/apollo-client/milestone/31) - June 10th, 2025 -_Release candidate - May 13th 2025_ +_Release candidate - May 15th 2025_ ### GraphQL Testing Library @@ -49,15 +49,9 @@ _These changes will take longer than anticipated due to prioritization on Apollo ### Apollo Client React Framework Integrations - New/more robust documentation -- Support for `@defer` with `PreloadQuery` (merged) - -**Next.js** -- Remove `experimental` label from Next.js integration (merged) - Public release March 31st, 2025 **TanStack Start** - Support for Apollo Client Streaming in TanStack Router - will stay alpha **React Router** - Support for Apollo Client Streaming in React Router 7 - will stay alpha - -Try out the [alpha](https://github.com/apollographql/apollo-client-nextjs/blob/next/README.md) today and give us feedback! From d264fef4d27505eddef48396aa2a6551f95aa809 Mon Sep 17 00:00:00 2001 From: Shane Myrick Date: Tue, 15 Apr 2025 17:45:20 -0700 Subject: [PATCH 60/68] [docs] Update cache-configuration keyFields docs (#11778) Co-authored-by: Jerel Miller --- docs/source/caching/cache-configuration.mdx | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/source/caching/cache-configuration.mdx b/docs/source/caching/cache-configuration.mdx index 808faefbc3f..209ee073bf6 100644 --- a/docs/source/caching/cache-configuration.mdx +++ b/docs/source/caching/cache-configuration.mdx @@ -180,6 +180,22 @@ const cache = new InMemoryCache({ // array for their keyFields. keyFields: [], }, + Store: { + // If you need to disable normalization, set the keyFields to false + // and the object will be embedded in the parent + keyFields: false + }, + Location: { + // You can also use a function to determine any of the values above. + // The first argument is the reference to the record to be written, and the second is the runtime context + keyFields: (location, context) => { + if (context.readField("state")) { + return ["city", "state", "country"] + } else { + return ["city", "country"] + } + } + } }, }); ``` @@ -195,6 +211,9 @@ This example shows a variety of `typePolicies` with different `keyFields`: Book:{"title":"Fahrenheit 451","author":{"name":"Ray Bradbury"}} ``` * The `AllProducts` type illustrates a special strategy for a **singleton** type. If the cache will only ever contain one `AllProducts` object and that object has _no_ identifying fields, you can provide an empty array for its `keyFields`. +* The `Store` type provides an example of how you can [disable normalization](https://www.apollographql.com/docs/react/caching/cache-configuration/#disabling-normalization) by setting the keyFields to `false` +* The `Location` type provides an example of using custom fuction given object and runtime context to calculate what fields should be used for the key field selection. + * Keep in mind that the first argument here is a reference to the record that will be written. As such, it does NOT include subselected fields, only scalar fields, and it contains aliases from the operation. If you need to read the real type you can use `context.storeObject`. To read even more indepth about how this function can work the best source will be our own [test cases for the cache policies](https://github.com/apollographql/apollo-client/blob/8bc7d4d406402962bf5151cdd2c5c75c9398d10c/src/cache/inmemory/__tests__/policies.ts#L5543-L5622) If an object has multiple `keyFields`, the cache ID always lists those fields in the same order to ensure uniqueness. From c19d41513cac0cc143aa7358f26c89c9408da102 Mon Sep 17 00:00:00 2001 From: Elias Gabriel Date: Thu, 17 Apr 2025 11:38:51 -0400 Subject: [PATCH 61/68] fix: cancellable in-flight multipart urql subscriptions (#12567) * fix: cancel in-flight multipart urql subscriptions at observer unsubscribe * add: changeset --- .changeset/hot-kangaroos-eat.md | 5 +++++ src/utilities/subscriptions/urql/index.ts | 8 +++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 .changeset/hot-kangaroos-eat.md diff --git a/.changeset/hot-kangaroos-eat.md b/.changeset/hot-kangaroos-eat.md new file mode 100644 index 00000000000..f8fe39e222b --- /dev/null +++ b/.changeset/hot-kangaroos-eat.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix in-flight multipart urql subscription cancellation diff --git a/src/utilities/subscriptions/urql/index.ts b/src/utilities/subscriptions/urql/index.ts index 26bf4d4fb57..a8f195562ec 100644 --- a/src/utilities/subscriptions/urql/index.ts +++ b/src/utilities/subscriptions/urql/index.ts @@ -35,7 +35,9 @@ export function createFetchMultipartSubscription( const currentFetch = preferredFetch || maybe(() => fetch) || backupFetch; const observerNext = observer.next.bind(observer); - currentFetch!(uri, options) + const abortController = new AbortController(); + + currentFetch!(uri, { ...options, signal: abortController.signal }) .then((response) => { const ctype = response.headers?.get("content-type"); @@ -51,6 +53,10 @@ export function createFetchMultipartSubscription( .catch((err: any) => { handleError(err, observer); }); + + return () => { + abortController.abort(); + }; }); }; } From 87d052108b51714a5f7015d4051732d0effa3fc5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 17 Apr 2025 10:38:51 -0600 Subject: [PATCH 62/68] Version Packages (#12571) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changeset/hot-kangaroos-eat.md | 5 ----- CHANGELOG.md | 6 ++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) delete mode 100644 .changeset/hot-kangaroos-eat.md diff --git a/.changeset/hot-kangaroos-eat.md b/.changeset/hot-kangaroos-eat.md deleted file mode 100644 index f8fe39e222b..00000000000 --- a/.changeset/hot-kangaroos-eat.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fix in-flight multipart urql subscription cancellation diff --git a/CHANGELOG.md b/CHANGELOG.md index 944442a64f9..3f9ff21d53b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/client +## 3.13.8 + +### Patch Changes + +- [#12567](https://github.com/apollographql/apollo-client/pull/12567) [`c19d415`](https://github.com/apollographql/apollo-client/commit/c19d41513cac0cc143aa7358f26c89c9408da102) Thanks [@thearchitector](https://github.com/thearchitector)! - Fix in-flight multipart urql subscription cancellation + ## 3.13.7 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 44d57906de1..f5fbd872075 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.13.7", + "version": "3.13.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.13.7", + "version": "3.13.8", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 983b3b657e6..8294b098ebb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.13.7", + "version": "3.13.8", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 03dc18e8fe8dd551a29b303a7ec071f565caa9d9 Mon Sep 17 00:00:00 2001 From: Daniel Abdelsamed Date: Mon, 21 Apr 2025 15:54:08 -0400 Subject: [PATCH 63/68] remove netlify client and update docs to mdx --- ...-link-context.md => apollo-link-context.mdx} | 0 ...ollo-link-error.md => apollo-link-error.mdx} | 0 ...apollo-link-http.md => apollo-link-http.mdx} | 0 ...apollo-link-rest.md => apollo-link-rest.mdx} | 0 ...ollo-link-retry.md => apollo-link-retry.mdx} | 0 ...lo-link-schema.md => apollo-link-schema.mdx} | 0 ...iptions.md => apollo-link-subscriptions.mdx} | 0 .../{apollo-link-ws.md => apollo-link-ws.mdx} | 0 .../{community-links.md => community-links.mdx} | 0 docs/source/api/react/{ssr.md => ssr.mdx} | 0 .../api/react/{testing.md => testing.mdx} | 0 .../data/{file-uploads.md => file-uploads.mdx} | 0 .../{static-typing.md => static-typing.mdx} | 0 .../{integrations.md => integrations.mdx} | 0 .../{react-native.md => react-native.mdx} | 0 .../integrations/{webpack.md => webpack.mdx} | 0 .../{hooks-migration.md => hooks-migration.mdx} | 0 ...tworking.md => advanced-http-networking.mdx} | 0 ...-networking.md => basic-http-networking.mdx} | 0 docs/source/performance/{babel.md => babel.mdx} | 0 docs/source/template.md | 17 ----------------- netlify.toml | 3 --- 22 files changed, 20 deletions(-) rename docs/source/api/link/{apollo-link-context.md => apollo-link-context.mdx} (100%) rename docs/source/api/link/{apollo-link-error.md => apollo-link-error.mdx} (100%) rename docs/source/api/link/{apollo-link-http.md => apollo-link-http.mdx} (100%) rename docs/source/api/link/{apollo-link-rest.md => apollo-link-rest.mdx} (100%) rename docs/source/api/link/{apollo-link-retry.md => apollo-link-retry.mdx} (100%) rename docs/source/api/link/{apollo-link-schema.md => apollo-link-schema.mdx} (100%) rename docs/source/api/link/{apollo-link-subscriptions.md => apollo-link-subscriptions.mdx} (100%) rename docs/source/api/link/{apollo-link-ws.md => apollo-link-ws.mdx} (100%) rename docs/source/api/link/{community-links.md => community-links.mdx} (100%) rename docs/source/api/react/{ssr.md => ssr.mdx} (100%) rename docs/source/api/react/{testing.md => testing.mdx} (100%) rename docs/source/data/{file-uploads.md => file-uploads.mdx} (100%) rename docs/source/development-testing/{static-typing.md => static-typing.mdx} (100%) rename docs/source/integrations/{integrations.md => integrations.mdx} (100%) rename docs/source/integrations/{react-native.md => react-native.mdx} (100%) rename docs/source/integrations/{webpack.md => webpack.mdx} (100%) rename docs/source/migrating/{hooks-migration.md => hooks-migration.mdx} (100%) rename docs/source/networking/{advanced-http-networking.md => advanced-http-networking.mdx} (100%) rename docs/source/networking/{basic-http-networking.md => basic-http-networking.mdx} (100%) rename docs/source/performance/{babel.md => babel.mdx} (100%) delete mode 100644 docs/source/template.md delete mode 100644 netlify.toml diff --git a/docs/source/api/link/apollo-link-context.md b/docs/source/api/link/apollo-link-context.mdx similarity index 100% rename from docs/source/api/link/apollo-link-context.md rename to docs/source/api/link/apollo-link-context.mdx diff --git a/docs/source/api/link/apollo-link-error.md b/docs/source/api/link/apollo-link-error.mdx similarity index 100% rename from docs/source/api/link/apollo-link-error.md rename to docs/source/api/link/apollo-link-error.mdx diff --git a/docs/source/api/link/apollo-link-http.md b/docs/source/api/link/apollo-link-http.mdx similarity index 100% rename from docs/source/api/link/apollo-link-http.md rename to docs/source/api/link/apollo-link-http.mdx diff --git a/docs/source/api/link/apollo-link-rest.md b/docs/source/api/link/apollo-link-rest.mdx similarity index 100% rename from docs/source/api/link/apollo-link-rest.md rename to docs/source/api/link/apollo-link-rest.mdx diff --git a/docs/source/api/link/apollo-link-retry.md b/docs/source/api/link/apollo-link-retry.mdx similarity index 100% rename from docs/source/api/link/apollo-link-retry.md rename to docs/source/api/link/apollo-link-retry.mdx diff --git a/docs/source/api/link/apollo-link-schema.md b/docs/source/api/link/apollo-link-schema.mdx similarity index 100% rename from docs/source/api/link/apollo-link-schema.md rename to docs/source/api/link/apollo-link-schema.mdx diff --git a/docs/source/api/link/apollo-link-subscriptions.md b/docs/source/api/link/apollo-link-subscriptions.mdx similarity index 100% rename from docs/source/api/link/apollo-link-subscriptions.md rename to docs/source/api/link/apollo-link-subscriptions.mdx diff --git a/docs/source/api/link/apollo-link-ws.md b/docs/source/api/link/apollo-link-ws.mdx similarity index 100% rename from docs/source/api/link/apollo-link-ws.md rename to docs/source/api/link/apollo-link-ws.mdx diff --git a/docs/source/api/link/community-links.md b/docs/source/api/link/community-links.mdx similarity index 100% rename from docs/source/api/link/community-links.md rename to docs/source/api/link/community-links.mdx diff --git a/docs/source/api/react/ssr.md b/docs/source/api/react/ssr.mdx similarity index 100% rename from docs/source/api/react/ssr.md rename to docs/source/api/react/ssr.mdx diff --git a/docs/source/api/react/testing.md b/docs/source/api/react/testing.mdx similarity index 100% rename from docs/source/api/react/testing.md rename to docs/source/api/react/testing.mdx diff --git a/docs/source/data/file-uploads.md b/docs/source/data/file-uploads.mdx similarity index 100% rename from docs/source/data/file-uploads.md rename to docs/source/data/file-uploads.mdx diff --git a/docs/source/development-testing/static-typing.md b/docs/source/development-testing/static-typing.mdx similarity index 100% rename from docs/source/development-testing/static-typing.md rename to docs/source/development-testing/static-typing.mdx diff --git a/docs/source/integrations/integrations.md b/docs/source/integrations/integrations.mdx similarity index 100% rename from docs/source/integrations/integrations.md rename to docs/source/integrations/integrations.mdx diff --git a/docs/source/integrations/react-native.md b/docs/source/integrations/react-native.mdx similarity index 100% rename from docs/source/integrations/react-native.md rename to docs/source/integrations/react-native.mdx diff --git a/docs/source/integrations/webpack.md b/docs/source/integrations/webpack.mdx similarity index 100% rename from docs/source/integrations/webpack.md rename to docs/source/integrations/webpack.mdx diff --git a/docs/source/migrating/hooks-migration.md b/docs/source/migrating/hooks-migration.mdx similarity index 100% rename from docs/source/migrating/hooks-migration.md rename to docs/source/migrating/hooks-migration.mdx diff --git a/docs/source/networking/advanced-http-networking.md b/docs/source/networking/advanced-http-networking.mdx similarity index 100% rename from docs/source/networking/advanced-http-networking.md rename to docs/source/networking/advanced-http-networking.mdx diff --git a/docs/source/networking/basic-http-networking.md b/docs/source/networking/basic-http-networking.mdx similarity index 100% rename from docs/source/networking/basic-http-networking.md rename to docs/source/networking/basic-http-networking.mdx diff --git a/docs/source/performance/babel.md b/docs/source/performance/babel.mdx similarity index 100% rename from docs/source/performance/babel.md rename to docs/source/performance/babel.mdx diff --git a/docs/source/template.md b/docs/source/template.md deleted file mode 100644 index 1137aee4f34..00000000000 --- a/docs/source/template.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Title -subtitle: Subtitle -description: Description ---- - -## Topic - -### Sub Topic - -```js -// code block -``` - -## Best Practices - -## API Documentation diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 97d5f4bb3d1..00000000000 --- a/netlify.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build] - publish = "docs/public" - command = "npm run typedoc; npm run docmodel > docs/public/log.txt || true" From fa4ceed29a74a650115f96c7f5339d13c61a6f06 Mon Sep 17 00:00:00 2001 From: Daniel Abdelsamed Date: Mon, 21 Apr 2025 16:05:49 -0400 Subject: [PATCH 64/68] wrap interpreted expressions <> and {} in code snippets --- docs/source/api/react/ssr.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/api/react/ssr.mdx b/docs/source/api/react/ssr.mdx index 661566b443f..dfa7a60e2a4 100644 --- a/docs/source/api/react/ssr.mdx +++ b/docs/source/api/react/ssr.mdx @@ -20,7 +20,7 @@ import { getDataFromTree } from "@apollo/client/react/ssr"; | Param | Type | description | | - | - | - | | `tree` | React.ReactNode | The React tree you would like to render and fetch data for. | -| `context` | { [key: string]: any } | Optional values you would like to make available in the React Context during rendering / data retrieval. | +| `context` | `{ [key: string]: any }` | Optional values you would like to make available in the React Context during rendering / data retrieval. | ### Result @@ -42,7 +42,7 @@ import { renderToStringWithData } from "@apollo/client/react/ssr"; | Param | Type | description | | - | - | - | -| `component` | ReactElement | The React component tree you would like to render and fetch data for. | +| `component` | `ReactElement` | The React component tree you would like to render and fetch data for. | ### Result From 4e2e3e8ed5461a9fb31ba0fa2d17db39fbba564c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 23 Apr 2025 09:13:49 -0600 Subject: [PATCH 65/68] Update ROADMAP.md --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index bebf56c3c93..070f45f134f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 🔮 Apollo Client Ecosystem Roadmap -**Last updated: 2025-04-11** +**Last updated: 2025-04-23** For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md). From fdf05c796d596178eea7744c0ebcb6ccae343a52 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 7 May 2025 08:55:15 -0600 Subject: [PATCH 66/68] Add caution to schema testing doc (#12596) Co-authored-by: Maria Elisabeth Schreiber --- .size-limits.json | 2 +- docs/source/development-testing/schema-driven-testing.mdx | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.size-limits.json b/.size-limits.json index ce52591ca48..9616d91d1cf 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 42331, + "dist/apollo-client.min.cjs": 42332, "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34542 } diff --git a/docs/source/development-testing/schema-driven-testing.mdx b/docs/source/development-testing/schema-driven-testing.mdx index aa07ea39d65..f398814acfb 100644 --- a/docs/source/development-testing/schema-driven-testing.mdx +++ b/docs/source/development-testing/schema-driven-testing.mdx @@ -4,6 +4,14 @@ description: Using createTestSchema and associated APIs minVersion: 3.10.0 --- + + The testing utilities described in this document are deprecated and will be + removed in [Apollo Client + v4](https://community.apollographql.com/t/apollo-client-v4-alpha-releases-are-here/8719) + in June 2025. Please use [GraphQL Testing + Library](https://github.com/apollographql/graphql-testing-library) instead. + + This article describes best practices for writing integration tests using testing utilities released as experimental in v3.10. These testing tools allow developers to execute queries against a schema configured with mock resolvers and default scalar values in order to test an entire Apollo Client application, including the [link chain](/react/api/link/introduction). ## Guiding principles From 7562a42dfbbd959682b7380409807d0ba7df54dc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 7 May 2025 09:24:47 -0600 Subject: [PATCH 67/68] Update ROADMAP.md --- ROADMAP.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 070f45f134f..c2b59ee316d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 🔮 Apollo Client Ecosystem Roadmap -**Last updated: 2025-04-23** +**Last updated: 2025-05-07** For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md). @@ -17,13 +17,13 @@ For up to date release notes, refer to the project's [Changelog](https://github. ### Apollo Client -#### 3.14.0 - May 20th, 2025 -_Release candidate - May 15th, 2025 +#### 3.14.0 - June 5th, 2025 +_Release candidate - May 29th, 2025 - 4.0 compatibility release/deprecations #### [4.0.0](https://github.com/apollographql/apollo-client/milestone/31) - June 10th, 2025 -_Release candidate - May 15th 2025_ +_Release candidate - May 29th 2025_ ### GraphQL Testing Library From ee9c6a6ccd273e325c6316da6021afd895c20e74 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 7 May 2025 10:36:03 -0600 Subject: [PATCH 68/68] Remove local resolvers deprecation (#12611) --- docs/source/local-state/local-resolvers.mdx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/source/local-state/local-resolvers.mdx b/docs/source/local-state/local-resolvers.mdx index 5a086b351f7..ff5b643ebac 100644 --- a/docs/source/local-state/local-resolvers.mdx +++ b/docs/source/local-state/local-resolvers.mdx @@ -3,10 +3,6 @@ title: Local resolvers description: Manage local data with GraphQL like resolvers --- -> 📄 **NOTE:** We recommend using field policies instead of local resolvers as described in [Local-only fields](./managing-state-with-field-policies/). -> -> Local resolver support will be moved out of the core of Apollo Client in a future major release. The same or similar functionality will be available via `ApolloLink`, as described in [this issue](https://github.com/apollographql/apollo-client/issues/10060). - We've learned how to manage remote data from our GraphQL server with Apollo Client, but what should we do with our local data? We want to be able to access boolean flags and device API results from multiple components in our app, but don't want to maintain a separate Redux or MobX store. Ideally, we would like the Apollo cache to be the single source of truth for all data in our client application. Apollo Client (>= 2.5) has built-in local state handling capabilities that allow you to store your local data inside the Apollo cache alongside your remote data. To access your local data, just query it with GraphQL. You can even request local and server data within the same query!