From 8df6013b6b45452ec058fab3e068b5b6d6c493f7 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 24 Apr 2024 12:38:00 +0200 Subject: [PATCH 01/62] MockLink: add query default variables if not specified in mock request resolves #8023 --- .changeset/fluffy-badgers-rush.md | 5 ++ .../core/mocking/__tests__/mockLink.ts | 84 ++++++++++++++++++- src/testing/core/mocking/mockLink.ts | 9 ++ .../internal/disposables/enableFakeTimers.ts | 26 ++++++ src/testing/internal/disposables/index.ts | 1 + 5 files changed, 122 insertions(+), 3 deletions(-) create mode 100644 .changeset/fluffy-badgers-rush.md create mode 100644 src/testing/internal/disposables/enableFakeTimers.ts diff --git a/.changeset/fluffy-badgers-rush.md b/.changeset/fluffy-badgers-rush.md new file mode 100644 index 00000000000..62f55a2b0f0 --- /dev/null +++ b/.changeset/fluffy-badgers-rush.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +MockLink: add query default variables if not specified in mock request diff --git a/src/testing/core/mocking/__tests__/mockLink.ts b/src/testing/core/mocking/__tests__/mockLink.ts index dc68b654505..119c9b1ba45 100644 --- a/src/testing/core/mocking/__tests__/mockLink.ts +++ b/src/testing/core/mocking/__tests__/mockLink.ts @@ -1,6 +1,7 @@ import gql from "graphql-tag"; import { MockLink, MockedResponse } from "../mockLink"; import { execute } from "../../../../link/core/execute"; +import { ObservableStream, enableFakeTimers } from "../../../internal"; describe("MockedResponse.newData", () => { const setup = () => { @@ -72,9 +73,6 @@ We've chosen this value as the MAXIMUM_DELAY since values that don't fit into a const MAXIMUM_DELAY = 0x7f_ff_ff_ff; describe("mockLink", () => { - beforeAll(() => jest.useFakeTimers()); - afterAll(() => jest.useRealTimers()); - const query = gql` query A { a @@ -82,6 +80,8 @@ describe("mockLink", () => { `; it("should not require a result or error when delay equals Infinity", async () => { + using _fakeTimers = enableFakeTimers(); + const mockLink = new MockLink([ { request: { @@ -103,6 +103,8 @@ describe("mockLink", () => { }); it("should require result or error when delay is just large", (done) => { + using _fakeTimers = enableFakeTimers(); + const mockLink = new MockLink([ { request: { @@ -125,4 +127,80 @@ describe("mockLink", () => { jest.advanceTimersByTime(MAXIMUM_DELAY); }); + + it("should fill in default variables if they are missing in mocked requests", async () => { + const query = gql` + query GetTodo($done: Boolean = true, $user: String!) { + todo(user: $user, done: $done) { + id + title + } + } + `; + const mocks = [ + { + // default should get filled in here + request: { query, variables: { user: "Tim" } }, + result: { + data: { todo: { id: 1 } }, + }, + }, + { + // we provide our own `done`, so it should not get filled in + request: { query, variables: { user: "Tim", done: false } }, + result: { + data: { todo: { id: 2 } }, + }, + }, + { + // one more that has a different user variable and should never match + request: { query, variables: { user: "Tom" } }, + result: { + data: { todo: { id: 2 } }, + }, + }, + ]; + + // Apollo Client will always fill in default values for missing variables + // in the operation before calling the Link, so we have to do the same here + // when we call `execute` + const defaults = { done: true }; + const link = new MockLink(mocks, false, { showWarnings: false }); + { + // Non-optional variable is missing, should not match. + const stream = new ObservableStream( + execute(link, { query, variables: { ...defaults } }) + ); + await stream.takeError(); + } + { + // Execute called incorrectly without a default variable filled in. + // This will never happen in Apollo Client since AC always fills these + // before calling `execute`, so it's okay if it results in a "no match" + // scenario here. + const stream = new ObservableStream( + execute(link, { query, variables: { user: "Tim" } }) + ); + await stream.takeError(); + } + { + // Expect default value to be filled in the mock request. + const stream = new ObservableStream( + execute(link, { query, variables: { ...defaults, user: "Tim" } }) + ); + const result = await stream.takeNext(); + expect(result).toEqual({ data: { todo: { id: 1 } } }); + } + { + // Test that defaults don't overwrite explicitly different values in a mock request. + const stream = new ObservableStream( + execute(link, { + query, + variables: { ...defaults, user: "Tim", done: false }, + }) + ); + const result = await stream.takeNext(); + expect(result).toEqual({ data: { todo: { id: 2 } } }); + } + }); }); diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index 46b43cca6ad..1258dae115d 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -17,6 +17,8 @@ import { cloneDeep, stringifyForDisplay, print, + getOperationDefinition, + getDefaultValues, } from "../../../utilities/index.js"; export type ResultFunction> = (variables: V) => T; @@ -212,6 +214,13 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} newMockedResponse.request.query = query; } + newMockedResponse.request.variables = { + ...getDefaultValues( + getOperationDefinition(newMockedResponse.request.query) + ), + ...newMockedResponse.request.variables, + }; + mockedResponse.maxUsageCount = mockedResponse.maxUsageCount ?? 1; invariant( mockedResponse.maxUsageCount > 0, diff --git a/src/testing/internal/disposables/enableFakeTimers.ts b/src/testing/internal/disposables/enableFakeTimers.ts new file mode 100644 index 00000000000..6be85d24730 --- /dev/null +++ b/src/testing/internal/disposables/enableFakeTimers.ts @@ -0,0 +1,26 @@ +import { withCleanup } from "./withCleanup.js"; + +declare global { + interface DateConstructor { + /* Jest uses @sinonjs/fake-timers, that add this flag */ + isFake: boolean; + } +} + +export function enableFakeTimers( + config?: FakeTimersConfig | LegacyFakeTimersConfig +) { + if (global.Date.isFake === true) { + // Nothing to do here, fake timers have already been set up. + // That also means we don't want to clean that up later. + return withCleanup({}, () => {}); + } + + jest.useFakeTimers(config); + return withCleanup({}, () => { + if (global.Date.isFake === true) { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + } + }); +} diff --git a/src/testing/internal/disposables/index.ts b/src/testing/internal/disposables/index.ts index 9895d129589..9d61c88fd90 100644 --- a/src/testing/internal/disposables/index.ts +++ b/src/testing/internal/disposables/index.ts @@ -1,3 +1,4 @@ export { disableActWarnings } from "./disableActWarnings.js"; export { spyOnConsole } from "./spyOnConsole.js"; export { withCleanup } from "./withCleanup.js"; +export { enableFakeTimers } from "./enableFakeTimers.js"; From bcd51e071b55c790116d7183022e83e6ed778737 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 24 Apr 2024 13:08:40 +0200 Subject: [PATCH 02/62] move logic --- src/testing/core/mocking/mockLink.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index 1258dae115d..46e6dbd760a 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -214,13 +214,6 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} newMockedResponse.request.query = query; } - newMockedResponse.request.variables = { - ...getDefaultValues( - getOperationDefinition(newMockedResponse.request.query) - ), - ...newMockedResponse.request.variables, - }; - mockedResponse.maxUsageCount = mockedResponse.maxUsageCount ?? 1; invariant( mockedResponse.maxUsageCount > 0, @@ -233,17 +226,21 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} } private normalizeVariableMatching(mockedResponse: MockedResponse) { - const variables = mockedResponse.request.variables; - if (mockedResponse.variableMatcher && variables) { + const request = mockedResponse.request; + if (mockedResponse.variableMatcher && request.variables) { throw new Error( "Mocked response should contain either variableMatcher or request.variables" ); } if (!mockedResponse.variableMatcher) { + request.variables = { + ...getDefaultValues(getOperationDefinition(request.query)), + ...request.variables, + }; mockedResponse.variableMatcher = (vars) => { const requestVariables = vars || {}; - const mockedResponseVariables = variables || {}; + const mockedResponseVariables = request.variables || {}; return equal(requestVariables, mockedResponseVariables); }; } From 7249ff6d9feffca15a36bbe3331ce98146a49747 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 2 May 2024 11:37:42 +0200 Subject: [PATCH 03/62] Update src/testing/core/mocking/__tests__/mockLink.ts Co-authored-by: Jerel Miller --- src/testing/core/mocking/__tests__/mockLink.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/testing/core/mocking/__tests__/mockLink.ts b/src/testing/core/mocking/__tests__/mockLink.ts index 119c9b1ba45..6c2d8e1d65a 100644 --- a/src/testing/core/mocking/__tests__/mockLink.ts +++ b/src/testing/core/mocking/__tests__/mockLink.ts @@ -133,7 +133,6 @@ describe("mockLink", () => { query GetTodo($done: Boolean = true, $user: String!) { todo(user: $user, done: $done) { id - title } } `; From ffb21ce82e46a819de4fae0d2f6560c787fb0404 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:28:23 +0000 Subject: [PATCH 04/62] 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..958803ab852 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,8 @@ +{ + "mode": "pre", + "tag": "alpha", + "initialVersions": { + "@apollo/client": "3.10.8" + }, + "changesets": [] +} From c2bd48b8ca8dabfad25f8b20772c7db0bd5a66bc Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 27 May 2024 17:40:34 +0200 Subject: [PATCH 05/62] extract hooks from InternalState --- src/react/hooks/useLazyQuery.ts | 4 +- src/react/hooks/useQuery.ts | 491 ++++++++++++++++---------------- 2 files changed, 253 insertions(+), 242 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index a8d6eb00a67..68172e06fc9 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -10,7 +10,7 @@ import type { LazyQueryResultTuple, NoInfer, } from "../types/types.js"; -import { useInternalState } from "./useQuery.js"; +import { useInternalState, useQueryWithInternalState } from "./useQuery.js"; import { useApolloClient } from "./useApolloClient.js"; // The following methods, when called will execute the query, regardless of @@ -85,7 +85,7 @@ export function useLazyQuery< document ); - const useQueryResult = internalState.useQuery({ + const useQueryResult = useQueryWithInternalState(internalState, { ...merged, skip: !execOptionsRef.current, }); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 61ca66527b6..46b4259f022 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -100,11 +100,121 @@ function _useQuery< query: DocumentNode | TypedDocumentNode, options: QueryHookOptions, NoInfer> ) { - return useInternalState(useApolloClient(options.client), query).useQuery( + return useQueryWithInternalState( + useInternalState(useApolloClient(options.client), query), options ); } +export function useQueryWithInternalState< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + internalState: InternalState, + options: QueryHookOptions, NoInfer> +) { + // The renderPromises field gets initialized here in the useQuery method, at + // the beginning of everything (for a given component rendering, at least), + // so we can safely use this.renderPromises in other/later InternalState + // methods without worrying it might be uninitialized. Even after + // initialization, this.renderPromises is usually undefined (unless SSR is + // happening), but that's fine as long as it has been initialized that way, + // rather than left uninitialized. + internalState.renderPromises = + React.useContext(getApolloContext()).renderPromises; + + useOptions(internalState, options); + + const obsQuery = useObservableQuery(internalState); + + const result = useSyncExternalStore( + React.useCallback( + (handleStoreChange) => { + if (internalState.renderPromises) { + return () => {}; + } + + internalState.forceUpdate = handleStoreChange; + + const onNext = () => { + const previousResult = internalState.result; + // We use `getCurrentResult()` instead of the onNext argument because + // the values differ slightly. Specifically, loading results will have + // an empty object for data instead of `undefined` for some reason. + const result = obsQuery.getCurrentResult(); + // Make sure we're not attempting to re-render similar results + if ( + previousResult && + previousResult.loading === result.loading && + previousResult.networkStatus === result.networkStatus && + equal(previousResult.data, result.data) + ) { + return; + } + + internalState.setResult(result); + }; + + const onError = (error: Error) => { + subscription.unsubscribe(); + subscription = obsQuery.resubscribeAfterError(onNext, onError); + + if (!hasOwnProperty.call(error, "graphQLErrors")) { + // The error is not a GraphQL error + throw error; + } + + const previousResult = internalState.result; + if ( + !previousResult || + (previousResult && previousResult.loading) || + !equal(error, previousResult.error) + ) { + internalState.setResult({ + data: (previousResult && previousResult.data) as TData, + error: error as ApolloError, + loading: false, + networkStatus: NetworkStatus.error, + }); + } + }; + + let subscription = obsQuery.subscribe(onNext, onError); + + // Do the "unsubscribe" with a short delay. + // This way, an existing subscription can be reused without an additional + // request if "unsubscribe" and "resubscribe" to the same ObservableQuery + // happen in very fast succession. + return () => { + setTimeout(() => subscription.unsubscribe()); + internalState.forceUpdate = () => internalState.forceUpdateState(); + }; + }, + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + [ + // We memoize the subscribe function using useCallback and the following + // dependency keys, because the subscribe function reference is all that + // useSyncExternalStore uses internally as a dependency key for the + // useEffect ultimately responsible for the subscription, so we are + // effectively passing this dependency array to that useEffect buried + // inside useSyncExternalStore, as desired. + obsQuery, + internalState.renderPromises, + internalState.client.disableNetworkFetches, + ] + ), + + () => internalState.getCurrentResult(), + () => internalState.getCurrentResult() + ); + + // TODO Remove this method when we remove support for options.partialRefetch. + internalState.unsafeHandlePartialRefetch(result); + + return internalState.toQueryResult(result); +} + export function useInternalState( client: ApolloClient, query: DocumentNode | TypedDocumentNode @@ -137,6 +247,128 @@ export function useInternalState( return state; } +function useOptions( + internalState: InternalState, + options: QueryHookOptions +) { + const watchQueryOptions = internalState.createWatchQueryOptions( + (internalState.queryHookOptions = options) + ); + + // Update this.watchQueryOptions, but only when they have changed, which + // allows us to depend on the referential stability of + // this.watchQueryOptions elsewhere. + const currentWatchQueryOptions = internalState.watchQueryOptions; + + if (!equal(watchQueryOptions, currentWatchQueryOptions)) { + internalState.watchQueryOptions = watchQueryOptions; + + if (currentWatchQueryOptions && internalState.observable) { + // Though it might be tempting to postpone this reobserve call to the + // useEffect block, we need getCurrentResult to return an appropriate + // loading:true result synchronously (later within the same call to + // useQuery). Since we already have this.observable here (not true for + // the very first call to useQuery), we are not initiating any new + // subscriptions, though it does feel less than ideal that reobserve + // (potentially) kicks off a network request (for example, when the + // variables have changed), which is technically a side-effect. + internalState.observable.reobserve(internalState.getObsQueryOptions()); + + // Make sure getCurrentResult returns a fresh ApolloQueryResult, + // but save the current data as this.previousData, just like setResult + // usually does. + internalState.previousData = + internalState.result?.data || internalState.previousData; + internalState.result = void 0; + } + } + + // Make sure state.onCompleted and state.onError always reflect the latest + // options.onCompleted and options.onError callbacks provided to useQuery, + // since those functions are often recreated every time useQuery is called. + // Like the forceUpdate method, the versions of these methods inherited from + // InternalState.prototype are empty no-ops, but we can override them on the + // base state object (without modifying the prototype). + internalState.onCompleted = + options.onCompleted || InternalState.prototype.onCompleted; + internalState.onError = options.onError || InternalState.prototype.onError; + + if ( + (internalState.renderPromises || + internalState.client.disableNetworkFetches) && + internalState.queryHookOptions.ssr === false && + !internalState.queryHookOptions.skip + ) { + // If SSR has been explicitly disabled, and this function has been called + // on the server side, return the default loading state. + internalState.result = internalState.ssrDisabledResult; + } else if ( + internalState.queryHookOptions.skip || + internalState.watchQueryOptions.fetchPolicy === "standby" + ) { + // When skipping a query (ie. we're not querying for data but still want to + // render children), make sure the `data` is cleared out and `loading` is + // set to `false` (since we aren't loading anything). + // + // NOTE: We no longer think this is the correct behavior. Skipping should + // not automatically set `data` to `undefined`, but instead leave the + // previous data in place. In other words, skipping should not mandate that + // previously received data is all of a sudden removed. Unfortunately, + // changing this is breaking, so we'll have to wait until Apollo Client 4.0 + // to address this. + internalState.result = internalState.skipStandbyResult; + } else if ( + internalState.result === internalState.ssrDisabledResult || + internalState.result === internalState.skipStandbyResult + ) { + internalState.result = void 0; + } +} + +function useObservableQuery( + internalState: InternalState +) { + // See if there is an existing observable that was used to fetch the same + // data and if so, use it instead since it will contain the proper queryId + // to fetch the result set. This is used during SSR. + const obsQuery = (internalState.observable = + (internalState.renderPromises && + internalState.renderPromises.getSSRObservable( + internalState.watchQueryOptions + )) || + internalState.observable || // Reuse this.observable if possible (and not SSR) + internalState.client.watchQuery(internalState.getObsQueryOptions())); + + internalState.obsQueryFields = React.useMemo( + () => ({ + refetch: obsQuery.refetch.bind(obsQuery), + reobserve: obsQuery.reobserve.bind(obsQuery), + fetchMore: obsQuery.fetchMore.bind(obsQuery), + updateQuery: obsQuery.updateQuery.bind(obsQuery), + startPolling: obsQuery.startPolling.bind(obsQuery), + stopPolling: obsQuery.stopPolling.bind(obsQuery), + subscribeToMore: obsQuery.subscribeToMore.bind(obsQuery), + }), + [obsQuery] + ); + + const ssrAllowed = !( + internalState.queryHookOptions.ssr === false || + internalState.queryHookOptions.skip + ); + + if (internalState.renderPromises && ssrAllowed) { + internalState.renderPromises.registerSSRObservable(obsQuery); + + if (obsQuery.getCurrentResult().loading) { + // TODO: This is a legacy API which could probably be cleaned up + internalState.renderPromises.addObservableQueryPromise(obsQuery); + } + } + + return obsQuery; +} + class InternalState { constructor( public readonly client: ReturnType, @@ -219,196 +451,15 @@ class InternalState { }); } - // Methods beginning with use- should be called according to the standard - // rules of React hooks: only at the top level of the calling function, and - // without any dynamic conditional logic. - useQuery(options: QueryHookOptions) { - // The renderPromises field gets initialized here in the useQuery method, at - // the beginning of everything (for a given component rendering, at least), - // so we can safely use this.renderPromises in other/later InternalState - // methods without worrying it might be uninitialized. Even after - // initialization, this.renderPromises is usually undefined (unless SSR is - // happening), but that's fine as long as it has been initialized that way, - // rather than left uninitialized. - // eslint-disable-next-line react-hooks/rules-of-hooks - this.renderPromises = React.useContext(getApolloContext()).renderPromises; - - this.useOptions(options); - - const obsQuery = this.useObservableQuery(); - - // eslint-disable-next-line react-hooks/rules-of-hooks - const result = useSyncExternalStore( - // eslint-disable-next-line react-hooks/rules-of-hooks - React.useCallback( - (handleStoreChange) => { - if (this.renderPromises) { - return () => {}; - } - - this.forceUpdate = handleStoreChange; - - const onNext = () => { - const previousResult = this.result; - // We use `getCurrentResult()` instead of the onNext argument because - // the values differ slightly. Specifically, loading results will have - // an empty object for data instead of `undefined` for some reason. - const result = obsQuery.getCurrentResult(); - // Make sure we're not attempting to re-render similar results - if ( - previousResult && - previousResult.loading === result.loading && - previousResult.networkStatus === result.networkStatus && - equal(previousResult.data, result.data) - ) { - return; - } - - this.setResult(result); - }; - - const onError = (error: Error) => { - subscription.unsubscribe(); - subscription = obsQuery.resubscribeAfterError(onNext, onError); - - if (!hasOwnProperty.call(error, "graphQLErrors")) { - // The error is not a GraphQL error - throw error; - } - - const previousResult = this.result; - if ( - !previousResult || - (previousResult && previousResult.loading) || - !equal(error, previousResult.error) - ) { - this.setResult({ - data: (previousResult && previousResult.data) as TData, - error: error as ApolloError, - loading: false, - networkStatus: NetworkStatus.error, - }); - } - }; - - let subscription = obsQuery.subscribe(onNext, onError); - - // Do the "unsubscribe" with a short delay. - // This way, an existing subscription can be reused without an additional - // request if "unsubscribe" and "resubscribe" to the same ObservableQuery - // happen in very fast succession. - return () => { - setTimeout(() => subscription.unsubscribe()); - this.forceUpdate = () => this.forceUpdateState(); - }; - }, - [ - // We memoize the subscribe function using useCallback and the following - // dependency keys, because the subscribe function reference is all that - // useSyncExternalStore uses internally as a dependency key for the - // useEffect ultimately responsible for the subscription, so we are - // effectively passing this dependency array to that useEffect buried - // inside useSyncExternalStore, as desired. - obsQuery, - // eslint-disable-next-line react-hooks/exhaustive-deps - this.renderPromises, - // eslint-disable-next-line react-hooks/exhaustive-deps - this.client.disableNetworkFetches, - ] - ), - - () => this.getCurrentResult(), - () => this.getCurrentResult() - ); - - // TODO Remove this method when we remove support for options.partialRefetch. - this.unsafeHandlePartialRefetch(result); - - return this.toQueryResult(result); - } - // These members (except for renderPromises) are all populated by the // useOptions method, which is called unconditionally at the beginning of the // useQuery method, so we can safely use these members in other/later methods // without worrying they might be uninitialized. - private renderPromises: ApolloContextValue["renderPromises"]; - private queryHookOptions!: QueryHookOptions; - private watchQueryOptions!: WatchQueryOptions; - - private useOptions(options: QueryHookOptions) { - const watchQueryOptions = this.createWatchQueryOptions( - (this.queryHookOptions = options) - ); - - // Update this.watchQueryOptions, but only when they have changed, which - // allows us to depend on the referential stability of - // this.watchQueryOptions elsewhere. - const currentWatchQueryOptions = this.watchQueryOptions; - - if (!equal(watchQueryOptions, currentWatchQueryOptions)) { - this.watchQueryOptions = watchQueryOptions; - - if (currentWatchQueryOptions && this.observable) { - // Though it might be tempting to postpone this reobserve call to the - // useEffect block, we need getCurrentResult to return an appropriate - // loading:true result synchronously (later within the same call to - // useQuery). Since we already have this.observable here (not true for - // the very first call to useQuery), we are not initiating any new - // subscriptions, though it does feel less than ideal that reobserve - // (potentially) kicks off a network request (for example, when the - // variables have changed), which is technically a side-effect. - this.observable.reobserve(this.getObsQueryOptions()); - - // Make sure getCurrentResult returns a fresh ApolloQueryResult, - // but save the current data as this.previousData, just like setResult - // usually does. - this.previousData = this.result?.data || this.previousData; - this.result = void 0; - } - } - - // Make sure state.onCompleted and state.onError always reflect the latest - // options.onCompleted and options.onError callbacks provided to useQuery, - // since those functions are often recreated every time useQuery is called. - // Like the forceUpdate method, the versions of these methods inherited from - // InternalState.prototype are empty no-ops, but we can override them on the - // base state object (without modifying the prototype). - this.onCompleted = - options.onCompleted || InternalState.prototype.onCompleted; - this.onError = options.onError || InternalState.prototype.onError; - - if ( - (this.renderPromises || this.client.disableNetworkFetches) && - this.queryHookOptions.ssr === false && - !this.queryHookOptions.skip - ) { - // If SSR has been explicitly disabled, and this function has been called - // on the server side, return the default loading state. - this.result = this.ssrDisabledResult; - } else if ( - this.queryHookOptions.skip || - this.watchQueryOptions.fetchPolicy === "standby" - ) { - // When skipping a query (ie. we're not querying for data but still want to - // render children), make sure the `data` is cleared out and `loading` is - // set to `false` (since we aren't loading anything). - // - // NOTE: We no longer think this is the correct behavior. Skipping should - // not automatically set `data` to `undefined`, but instead leave the - // previous data in place. In other words, skipping should not mandate that - // previously received data is all of a sudden removed. Unfortunately, - // changing this is breaking, so we'll have to wait until Apollo Client 4.0 - // to address this. - this.result = this.skipStandbyResult; - } else if ( - this.result === this.ssrDisabledResult || - this.result === this.skipStandbyResult - ) { - this.result = void 0; - } - } + public renderPromises: ApolloContextValue["renderPromises"]; + public queryHookOptions!: QueryHookOptions; + public watchQueryOptions!: WatchQueryOptions; - private getObsQueryOptions(): WatchQueryOptions { + public getObsQueryOptions(): WatchQueryOptions { const toMerge: Array>> = []; const globalDefaults = this.client.defaultOptions.watchQuery; @@ -438,14 +489,14 @@ class InternalState { return toMerge.reduce(mergeOptions) as WatchQueryOptions; } - private ssrDisabledResult = maybeDeepFreeze({ + public ssrDisabledResult = maybeDeepFreeze({ loading: true, data: void 0 as unknown as TData, error: void 0, networkStatus: NetworkStatus.loading, }); - private skipStandbyResult = maybeDeepFreeze({ + public skipStandbyResult = maybeDeepFreeze({ loading: false, data: void 0 as unknown as TData, error: void 0, @@ -453,7 +504,7 @@ class InternalState { }); // A function to massage options before passing them to ObservableQuery. - private createWatchQueryOptions({ + public createWatchQueryOptions({ skip, ssr, onCompleted, @@ -519,61 +570,21 @@ class InternalState { // Defining these methods as no-ops on the prototype allows us to call // state.onCompleted and/or state.onError without worrying about whether a // callback was provided. - private onCompleted(data: TData) {} - private onError(error: ApolloError) {} + public onCompleted(data: TData) {} + public onError(error: ApolloError) {} - private observable!: ObservableQuery; + public observable!: ObservableQuery; public obsQueryFields!: Omit< ObservableQueryFields, "variables" >; - private useObservableQuery() { - // See if there is an existing observable that was used to fetch the same - // data and if so, use it instead since it will contain the proper queryId - // to fetch the result set. This is used during SSR. - const obsQuery = (this.observable = - (this.renderPromises && - this.renderPromises.getSSRObservable(this.watchQueryOptions)) || - this.observable || // Reuse this.observable if possible (and not SSR) - this.client.watchQuery(this.getObsQueryOptions())); - - // eslint-disable-next-line react-hooks/rules-of-hooks - this.obsQueryFields = React.useMemo( - () => ({ - refetch: obsQuery.refetch.bind(obsQuery), - reobserve: obsQuery.reobserve.bind(obsQuery), - fetchMore: obsQuery.fetchMore.bind(obsQuery), - updateQuery: obsQuery.updateQuery.bind(obsQuery), - startPolling: obsQuery.startPolling.bind(obsQuery), - stopPolling: obsQuery.stopPolling.bind(obsQuery), - subscribeToMore: obsQuery.subscribeToMore.bind(obsQuery), - }), - [obsQuery] - ); - - const ssrAllowed = !( - this.queryHookOptions.ssr === false || this.queryHookOptions.skip - ); - - if (this.renderPromises && ssrAllowed) { - this.renderPromises.registerSSRObservable(obsQuery); - - if (obsQuery.getCurrentResult().loading) { - // TODO: This is a legacy API which could probably be cleaned up - this.renderPromises.addObservableQueryPromise(obsQuery); - } - } - - return obsQuery; - } - // These members are populated by getCurrentResult and setResult, and it's // okay/normal for them to be initially undefined. - private result: undefined | ApolloQueryResult; - private previousData: undefined | TData; + public result: undefined | ApolloQueryResult; + public previousData: undefined | TData; - private setResult(nextResult: ApolloQueryResult) { + public setResult(nextResult: ApolloQueryResult) { const previousResult = this.result; if (previousResult && previousResult.data) { this.previousData = previousResult.data; @@ -585,7 +596,7 @@ class InternalState { this.handleErrorOrCompleted(nextResult, previousResult); } - private handleErrorOrCompleted( + public handleErrorOrCompleted( result: ApolloQueryResult, previousResult?: ApolloQueryResult ) { @@ -611,7 +622,7 @@ class InternalState { } } - private toApolloError( + public toApolloError( result: ApolloQueryResult ): ApolloError | undefined { return isNonEmptyArray(result.errors) ? @@ -619,7 +630,7 @@ class InternalState { : result.error; } - private getCurrentResult(): ApolloQueryResult { + public getCurrentResult(): ApolloQueryResult { // Using this.result as a cache ensures getCurrentResult continues returning // the same (===) result object, unless state.setResult has been called, or // we're doing server rendering and therefore override the result below. @@ -634,7 +645,7 @@ class InternalState { // This cache allows the referential stability of this.result (as returned by // getCurrentResult) to translate into referential stability of the resulting // QueryResult object returned by toQueryResult. - private toQueryResultCache = new (canUseWeakMap ? WeakMap : Map)< + public toQueryResultCache = new (canUseWeakMap ? WeakMap : Map)< ApolloQueryResult, QueryResult >(); @@ -671,7 +682,7 @@ class InternalState { return queryResult; } - private unsafeHandlePartialRefetch(result: ApolloQueryResult) { + public unsafeHandlePartialRefetch(result: ApolloQueryResult) { // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION // // TODO: This code should be removed when the partialRefetch option is From dc58beb82a2f15b9fb2733e6f4ab562855680af4 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 27 May 2024 18:00:33 +0200 Subject: [PATCH 06/62] inline hooks and functions into each other, move `createWatchQueryOptions` out --- src/react/hooks/useLazyQuery.ts | 2 +- src/react/hooks/useQuery.ts | 349 +++++++++++++++----------------- 2 files changed, 168 insertions(+), 183 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 68172e06fc9..e44f79a8464 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -142,7 +142,7 @@ export function useLazyQuery< }); const promise = internalState - .executeQuery({ ...options, skip: false }) + .executeQuery({ ...options, skip: false }, false) .then((queryResult) => Object.assign(queryResult, eagerMethods)); // Because the return value of `useLazyQuery` is usually floated, we need diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 46b4259f022..4d6fa35d732 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -9,7 +9,6 @@ import type { WatchQueryFetchPolicy, } from "../../core/index.js"; import { mergeOptions } from "../../utilities/index.js"; -import type { ApolloContextValue } from "../context/index.js"; import { getApolloContext } from "../context/index.js"; import { ApolloError } from "../../errors/index.js"; import type { @@ -120,17 +119,122 @@ export function useQueryWithInternalState< // initialization, this.renderPromises is usually undefined (unless SSR is // happening), but that's fine as long as it has been initialized that way, // rather than left uninitialized. - internalState.renderPromises = - React.useContext(getApolloContext()).renderPromises; + const renderPromises = React.useContext(getApolloContext()).renderPromises; - useOptions(internalState, options); + const watchQueryOptions = createWatchQueryOptions( + (internalState.queryHookOptions = options), + internalState, + !!renderPromises + ); + + // Update this.watchQueryOptions, but only when they have changed, which + // allows us to depend on the referential stability of + // this.watchQueryOptions elsewhere. + const currentWatchQueryOptions = internalState.watchQueryOptions; + + if (!equal(watchQueryOptions, currentWatchQueryOptions)) { + internalState.watchQueryOptions = watchQueryOptions; + + if (currentWatchQueryOptions && internalState.observable) { + // Though it might be tempting to postpone this reobserve call to the + // useEffect block, we need getCurrentResult to return an appropriate + // loading:true result synchronously (later within the same call to + // useQuery). Since we already have this.observable here (not true for + // the very first call to useQuery), we are not initiating any new + // subscriptions, though it does feel less than ideal that reobserve + // (potentially) kicks off a network request (for example, when the + // variables have changed), which is technically a side-effect. + internalState.observable.reobserve(internalState.getObsQueryOptions()); + + // Make sure getCurrentResult returns a fresh ApolloQueryResult, + // but save the current data as this.previousData, just like setResult + // usually does. + internalState.previousData = + internalState.result?.data || internalState.previousData; + internalState.result = void 0; + } + } - const obsQuery = useObservableQuery(internalState); + // Make sure state.onCompleted and state.onError always reflect the latest + // options.onCompleted and options.onError callbacks provided to useQuery, + // since those functions are often recreated every time useQuery is called. + // Like the forceUpdate method, the versions of these methods inherited from + // InternalState.prototype are empty no-ops, but we can override them on the + // base state object (without modifying the prototype). + internalState.onCompleted = + options.onCompleted || InternalState.prototype.onCompleted; + internalState.onError = options.onError || InternalState.prototype.onError; + + if ( + (renderPromises || internalState.client.disableNetworkFetches) && + internalState.queryHookOptions.ssr === false && + !internalState.queryHookOptions.skip + ) { + // If SSR has been explicitly disabled, and this function has been called + // on the server side, return the default loading state. + internalState.result = internalState.ssrDisabledResult; + } else if ( + internalState.queryHookOptions.skip || + internalState.watchQueryOptions.fetchPolicy === "standby" + ) { + // When skipping a query (ie. we're not querying for data but still want to + // render children), make sure the `data` is cleared out and `loading` is + // set to `false` (since we aren't loading anything). + // + // NOTE: We no longer think this is the correct behavior. Skipping should + // not automatically set `data` to `undefined`, but instead leave the + // previous data in place. In other words, skipping should not mandate that + // previously received data is all of a sudden removed. Unfortunately, + // changing this is breaking, so we'll have to wait until Apollo Client 4.0 + // to address this. + internalState.result = internalState.skipStandbyResult; + } else if ( + internalState.result === internalState.ssrDisabledResult || + internalState.result === internalState.skipStandbyResult + ) { + internalState.result = void 0; + } + + // See if there is an existing observable that was used to fetch the same + // data and if so, use it instead since it will contain the proper queryId + // to fetch the result set. This is used during SSR. + const obsQuery = (internalState.observable = + (renderPromises && + renderPromises.getSSRObservable(internalState.watchQueryOptions)) || + internalState.observable || // Reuse this.observable if possible (and not SSR) + internalState.client.watchQuery(internalState.getObsQueryOptions())); + + internalState.obsQueryFields = React.useMemo( + () => ({ + refetch: obsQuery.refetch.bind(obsQuery), + reobserve: obsQuery.reobserve.bind(obsQuery), + fetchMore: obsQuery.fetchMore.bind(obsQuery), + updateQuery: obsQuery.updateQuery.bind(obsQuery), + startPolling: obsQuery.startPolling.bind(obsQuery), + stopPolling: obsQuery.stopPolling.bind(obsQuery), + subscribeToMore: obsQuery.subscribeToMore.bind(obsQuery), + }), + [obsQuery] + ); + + const ssrAllowed = !( + internalState.queryHookOptions.ssr === false || + internalState.queryHookOptions.skip + ); + + if (renderPromises && ssrAllowed) { + renderPromises.registerSSRObservable(obsQuery); + + if (obsQuery.getCurrentResult().loading) { + // TODO: This is a legacy API which could probably be cleaned up + renderPromises.addObservableQueryPromise(obsQuery); + } + } const result = useSyncExternalStore( React.useCallback( (handleStoreChange) => { - if (internalState.renderPromises) { + if (renderPromises) { return () => {}; } @@ -200,7 +304,7 @@ export function useQueryWithInternalState< // effectively passing this dependency array to that useEffect buried // inside useSyncExternalStore, as desired. obsQuery, - internalState.renderPromises, + renderPromises, internalState.client.disableNetworkFetches, ] ), @@ -246,127 +350,66 @@ export function useInternalState( return state; } - -function useOptions( +// A function to massage options before passing them to ObservableQuery. +function createWatchQueryOptions< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + { + skip, + ssr, + onCompleted, + onError, + defaultOptions, + // The above options are useQuery-specific, so this ...otherOptions spread + // makes otherOptions almost a WatchQueryOptions object, except for the + // query property that we add below. + ...otherOptions + }: QueryHookOptions = {}, internalState: InternalState, - options: QueryHookOptions -) { - const watchQueryOptions = internalState.createWatchQueryOptions( - (internalState.queryHookOptions = options) + hasRenderPromises: boolean +): WatchQueryOptions { + // This Object.assign is safe because otherOptions is a fresh ...rest object + // that did not exist until just now, so modifications are still allowed. + const watchQueryOptions: WatchQueryOptions = Object.assign( + otherOptions, + { query: internalState.query } ); - // Update this.watchQueryOptions, but only when they have changed, which - // allows us to depend on the referential stability of - // this.watchQueryOptions elsewhere. - const currentWatchQueryOptions = internalState.watchQueryOptions; - - if (!equal(watchQueryOptions, currentWatchQueryOptions)) { - internalState.watchQueryOptions = watchQueryOptions; - - if (currentWatchQueryOptions && internalState.observable) { - // Though it might be tempting to postpone this reobserve call to the - // useEffect block, we need getCurrentResult to return an appropriate - // loading:true result synchronously (later within the same call to - // useQuery). Since we already have this.observable here (not true for - // the very first call to useQuery), we are not initiating any new - // subscriptions, though it does feel less than ideal that reobserve - // (potentially) kicks off a network request (for example, when the - // variables have changed), which is technically a side-effect. - internalState.observable.reobserve(internalState.getObsQueryOptions()); - - // Make sure getCurrentResult returns a fresh ApolloQueryResult, - // but save the current data as this.previousData, just like setResult - // usually does. - internalState.previousData = - internalState.result?.data || internalState.previousData; - internalState.result = void 0; - } - } - - // Make sure state.onCompleted and state.onError always reflect the latest - // options.onCompleted and options.onError callbacks provided to useQuery, - // since those functions are often recreated every time useQuery is called. - // Like the forceUpdate method, the versions of these methods inherited from - // InternalState.prototype are empty no-ops, but we can override them on the - // base state object (without modifying the prototype). - internalState.onCompleted = - options.onCompleted || InternalState.prototype.onCompleted; - internalState.onError = options.onError || InternalState.prototype.onError; - if ( - (internalState.renderPromises || - internalState.client.disableNetworkFetches) && - internalState.queryHookOptions.ssr === false && - !internalState.queryHookOptions.skip - ) { - // If SSR has been explicitly disabled, and this function has been called - // on the server side, return the default loading state. - internalState.result = internalState.ssrDisabledResult; - } else if ( - internalState.queryHookOptions.skip || - internalState.watchQueryOptions.fetchPolicy === "standby" - ) { - // When skipping a query (ie. we're not querying for data but still want to - // render children), make sure the `data` is cleared out and `loading` is - // set to `false` (since we aren't loading anything). - // - // NOTE: We no longer think this is the correct behavior. Skipping should - // not automatically set `data` to `undefined`, but instead leave the - // previous data in place. In other words, skipping should not mandate that - // previously received data is all of a sudden removed. Unfortunately, - // changing this is breaking, so we'll have to wait until Apollo Client 4.0 - // to address this. - internalState.result = internalState.skipStandbyResult; - } else if ( - internalState.result === internalState.ssrDisabledResult || - internalState.result === internalState.skipStandbyResult + hasRenderPromises && + (watchQueryOptions.fetchPolicy === "network-only" || + watchQueryOptions.fetchPolicy === "cache-and-network") ) { - internalState.result = void 0; + // this behavior was added to react-apollo without explanation in this PR + // https://github.com/apollographql/react-apollo/pull/1579 + watchQueryOptions.fetchPolicy = "cache-first"; } -} -function useObservableQuery( - internalState: InternalState -) { - // See if there is an existing observable that was used to fetch the same - // data and if so, use it instead since it will contain the proper queryId - // to fetch the result set. This is used during SSR. - const obsQuery = (internalState.observable = - (internalState.renderPromises && - internalState.renderPromises.getSSRObservable( - internalState.watchQueryOptions - )) || - internalState.observable || // Reuse this.observable if possible (and not SSR) - internalState.client.watchQuery(internalState.getObsQueryOptions())); - - internalState.obsQueryFields = React.useMemo( - () => ({ - refetch: obsQuery.refetch.bind(obsQuery), - reobserve: obsQuery.reobserve.bind(obsQuery), - fetchMore: obsQuery.fetchMore.bind(obsQuery), - updateQuery: obsQuery.updateQuery.bind(obsQuery), - startPolling: obsQuery.startPolling.bind(obsQuery), - stopPolling: obsQuery.stopPolling.bind(obsQuery), - subscribeToMore: obsQuery.subscribeToMore.bind(obsQuery), - }), - [obsQuery] - ); - - const ssrAllowed = !( - internalState.queryHookOptions.ssr === false || - internalState.queryHookOptions.skip - ); - - if (internalState.renderPromises && ssrAllowed) { - internalState.renderPromises.registerSSRObservable(obsQuery); + if (!watchQueryOptions.variables) { + watchQueryOptions.variables = {} as TVariables; + } - if (obsQuery.getCurrentResult().loading) { - // TODO: This is a legacy API which could probably be cleaned up - internalState.renderPromises.addObservableQueryPromise(obsQuery); - } + if (skip) { + const { + fetchPolicy = internalState.getDefaultFetchPolicy(), + initialFetchPolicy = fetchPolicy, + } = watchQueryOptions; + + // When skipping, we set watchQueryOptions.fetchPolicy initially to + // "standby", but we also need/want to preserve the initial non-standby + // fetchPolicy that would have been used if not skipping. + Object.assign(watchQueryOptions, { + initialFetchPolicy, + fetchPolicy: "standby", + }); + } else if (!watchQueryOptions.fetchPolicy) { + watchQueryOptions.fetchPolicy = + internalState.observable?.options.initialFetchPolicy || + internalState.getDefaultFetchPolicy(); } - return obsQuery; + return watchQueryOptions; } class InternalState { @@ -409,14 +452,17 @@ class InternalState { executeQuery( options: QueryHookOptions & { query?: DocumentNode; - } + }, + hasRenderPromises: boolean ) { if (options.query) { Object.assign(this, { query: options.query }); } - this.watchQueryOptions = this.createWatchQueryOptions( - (this.queryHookOptions = options) + this.watchQueryOptions = createWatchQueryOptions( + (this.queryHookOptions = options), + this, + hasRenderPromises ); const concast = this.observable.reobserveAsConcast( @@ -451,11 +497,6 @@ class InternalState { }); } - // These members (except for renderPromises) are all populated by the - // useOptions method, which is called unconditionally at the beginning of the - // useQuery method, so we can safely use these members in other/later methods - // without worrying they might be uninitialized. - public renderPromises: ApolloContextValue["renderPromises"]; public queryHookOptions!: QueryHookOptions; public watchQueryOptions!: WatchQueryOptions; @@ -503,62 +544,6 @@ class InternalState { networkStatus: NetworkStatus.ready, }); - // A function to massage options before passing them to ObservableQuery. - public createWatchQueryOptions({ - skip, - ssr, - onCompleted, - onError, - defaultOptions, - // The above options are useQuery-specific, so this ...otherOptions spread - // makes otherOptions almost a WatchQueryOptions object, except for the - // query property that we add below. - ...otherOptions - }: QueryHookOptions = {}): WatchQueryOptions< - TVariables, - TData - > { - // This Object.assign is safe because otherOptions is a fresh ...rest object - // that did not exist until just now, so modifications are still allowed. - const watchQueryOptions: WatchQueryOptions = - Object.assign(otherOptions, { query: this.query }); - - if ( - this.renderPromises && - (watchQueryOptions.fetchPolicy === "network-only" || - watchQueryOptions.fetchPolicy === "cache-and-network") - ) { - // this behavior was added to react-apollo without explanation in this PR - // https://github.com/apollographql/react-apollo/pull/1579 - watchQueryOptions.fetchPolicy = "cache-first"; - } - - if (!watchQueryOptions.variables) { - watchQueryOptions.variables = {} as TVariables; - } - - if (skip) { - const { - fetchPolicy = this.getDefaultFetchPolicy(), - initialFetchPolicy = fetchPolicy, - } = watchQueryOptions; - - // When skipping, we set watchQueryOptions.fetchPolicy initially to - // "standby", but we also need/want to preserve the initial non-standby - // fetchPolicy that would have been used if not skipping. - Object.assign(watchQueryOptions, { - initialFetchPolicy, - fetchPolicy: "standby", - }); - } else if (!watchQueryOptions.fetchPolicy) { - watchQueryOptions.fetchPolicy = - this.observable?.options.initialFetchPolicy || - this.getDefaultFetchPolicy(); - } - - return watchQueryOptions; - } - getDefaultFetchPolicy(): WatchQueryFetchPolicy { return ( this.queryHookOptions.defaultOptions?.fetchPolicy || From 10466e5c63defffee66f5c03a58daa0b02dd44d5 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 27 May 2024 18:12:29 +0200 Subject: [PATCH 07/62] move `executeQuery` to `useLazyQuery` --- src/react/hooks/useLazyQuery.ts | 83 ++++++++++++++++++++++-- src/react/hooks/useQuery.ts | 111 ++++++-------------------------- 2 files changed, 95 insertions(+), 99 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index e44f79a8464..a6e3350a428 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -2,15 +2,25 @@ import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import * as React from "rehackt"; -import type { OperationVariables } from "../../core/index.js"; +import type { + ApolloQueryResult, + OperationVariables, +} from "../../core/index.js"; import { mergeOptions } from "../../utilities/index.js"; import type { LazyQueryHookExecOptions, LazyQueryHookOptions, LazyQueryResultTuple, NoInfer, + QueryHookOptions, + QueryResult, } from "../types/types.js"; -import { useInternalState, useQueryWithInternalState } from "./useQuery.js"; +import type { InternalState } from "./useQuery.js"; +import { + createWatchQueryOptions, + useInternalState, + useQueryWithInternalState, +} from "./useQuery.js"; import { useApolloClient } from "./useApolloClient.js"; // The following methods, when called will execute the query, regardless of @@ -94,7 +104,8 @@ export function useLazyQuery< useQueryResult.observable.options.initialFetchPolicy || internalState.getDefaultFetchPolicy(); - const { forceUpdateState, obsQueryFields } = internalState; + const { obsQueryFields } = internalState; + const forceUpdateState = React.useReducer((tick) => tick + 1, 0)[1]; // We use useMemo here to make sure the eager methods have a stable identity. const eagerMethods = React.useMemo(() => { const eagerMethods: Record = {}; @@ -141,9 +152,12 @@ export function useLazyQuery< ...execOptionsRef.current, }); - const promise = internalState - .executeQuery({ ...options, skip: false }, false) - .then((queryResult) => Object.assign(queryResult, eagerMethods)); + const promise = executeQuery( + { ...options, skip: false }, + false, + internalState, + forceUpdateState + ).then((queryResult) => Object.assign(queryResult, eagerMethods)); // Because the return value of `useLazyQuery` is usually floated, we need // to catch the promise to prevent unhandled rejections. @@ -151,8 +165,63 @@ export function useLazyQuery< return promise; }, - [eagerMethods, initialFetchPolicy, internalState] + [eagerMethods, forceUpdateState, initialFetchPolicy, internalState] ); return [execute, result]; } + +function executeQuery( + options: QueryHookOptions & { + query?: DocumentNode; + }, + hasRenderPromises: boolean, + internalState: InternalState, + forceUpdate: () => void +) { + if (options.query) { + internalState.query = options.query; + } + + internalState.watchQueryOptions = createWatchQueryOptions( + (internalState.queryHookOptions = options), + internalState, + hasRenderPromises + ); + + const concast = internalState.observable.reobserveAsConcast( + internalState.getObsQueryOptions() + ); + + // Make sure getCurrentResult returns a fresh ApolloQueryResult, + // but save the current data as this.previousData, just like setResult + // usually does. + internalState.previousData = + internalState.result?.data || internalState.previousData; + internalState.result = void 0; + forceUpdate(); + + return new Promise>((resolve) => { + let result: ApolloQueryResult; + + // Subscribe to the concast independently of the ObservableQuery in case + // the component gets unmounted before the promise resolves. This prevents + // the concast from terminating early and resolving with `undefined` when + // there are no more subscribers for the concast. + concast.subscribe({ + next: (value) => { + result = value; + }, + error: () => { + resolve( + internalState.toQueryResult( + internalState.observable.getCurrentResult() + ) + ); + }, + complete: () => { + resolve(internalState.toQueryResult(result)); + }, + }); + }); +} diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 4d6fa35d732..c2fdc84c186 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -238,8 +238,6 @@ export function useQueryWithInternalState< return () => {}; } - internalState.forceUpdate = handleStoreChange; - const onNext = () => { const previousResult = internalState.result; // We use `getCurrentResult()` instead of the onNext argument because @@ -256,7 +254,7 @@ export function useQueryWithInternalState< return; } - internalState.setResult(result); + internalState.setResult(result, handleStoreChange); }; const onError = (error: Error) => { @@ -274,12 +272,15 @@ export function useQueryWithInternalState< (previousResult && previousResult.loading) || !equal(error, previousResult.error) ) { - internalState.setResult({ - data: (previousResult && previousResult.data) as TData, - error: error as ApolloError, - loading: false, - networkStatus: NetworkStatus.error, - }); + internalState.setResult( + { + data: (previousResult && previousResult.data) as TData, + error: error as ApolloError, + loading: false, + networkStatus: NetworkStatus.error, + }, + handleStoreChange + ); } }; @@ -291,7 +292,6 @@ export function useQueryWithInternalState< // happen in very fast succession. return () => { setTimeout(() => subscription.unsubscribe()); - internalState.forceUpdate = () => internalState.forceUpdateState(); }; }, // eslint-disable-next-line react-compiler/react-compiler @@ -323,17 +323,8 @@ export function useInternalState( client: ApolloClient, query: DocumentNode | TypedDocumentNode ): InternalState { - // By default, InternalState.prototype.forceUpdate is an empty function, but - // we replace it here (before anyone has had a chance to see this state yet) - // with a function that unconditionally forces an update, using the latest - // setTick function. Updating this state by calling state.forceUpdate or the - // uSES notification callback are the only way we trigger React component updates. - const forceUpdateState = React.useReducer((tick) => tick + 1, 0)[1]; - function createInternalState(previous?: InternalState) { - return Object.assign(new InternalState(client, query, previous), { - forceUpdateState, - }); + return new InternalState(client, query, previous); } let [state, updateState] = React.useState(createInternalState); @@ -351,7 +342,7 @@ export function useInternalState( return state; } // A function to massage options before passing them to ObservableQuery. -function createWatchQueryOptions< +export function createWatchQueryOptions< TData = any, TVariables extends OperationVariables = OperationVariables, >( @@ -412,10 +403,11 @@ function createWatchQueryOptions< return watchQueryOptions; } +export { type InternalState }; class InternalState { constructor( public readonly client: ReturnType, - public readonly query: DocumentNode | TypedDocumentNode, + public query: DocumentNode | TypedDocumentNode, previous?: InternalState ) { verifyDocumentType(query, DocumentType.Query); @@ -429,74 +421,6 @@ class InternalState { } } - /** - * Forces an update using local component state. - * As this is not batched with `useSyncExternalStore` updates, - * this is only used as a fallback if the `useSyncExternalStore` "force update" - * method is not registered at the moment. - * See https://github.com/facebook/react/issues/25191 - * */ - forceUpdateState() { - // Replaced (in useInternalState) with a method that triggers an update. - invariant.warn( - "Calling default no-op implementation of InternalState#forceUpdate" - ); - } - - /** - * Will be overwritten by the `useSyncExternalStore` "force update" method - * whenever it is available and reset to `forceUpdateState` when it isn't. - */ - forceUpdate = () => this.forceUpdateState(); - - executeQuery( - options: QueryHookOptions & { - query?: DocumentNode; - }, - hasRenderPromises: boolean - ) { - if (options.query) { - Object.assign(this, { query: options.query }); - } - - this.watchQueryOptions = createWatchQueryOptions( - (this.queryHookOptions = options), - this, - hasRenderPromises - ); - - const concast = this.observable.reobserveAsConcast( - this.getObsQueryOptions() - ); - - // Make sure getCurrentResult returns a fresh ApolloQueryResult, - // but save the current data as this.previousData, just like setResult - // usually does. - this.previousData = this.result?.data || this.previousData; - this.result = void 0; - this.forceUpdate(); - - return new Promise>((resolve) => { - let result: ApolloQueryResult; - - // Subscribe to the concast independently of the ObservableQuery in case - // the component gets unmounted before the promise resolves. This prevents - // the concast from terminating early and resolving with `undefined` when - // there are no more subscribers for the concast. - concast.subscribe({ - next: (value) => { - result = value; - }, - error: () => { - resolve(this.toQueryResult(this.observable.getCurrentResult())); - }, - complete: () => { - resolve(this.toQueryResult(result)); - }, - }); - }); - } - public queryHookOptions!: QueryHookOptions; public watchQueryOptions!: WatchQueryOptions; @@ -569,7 +493,10 @@ class InternalState { public result: undefined | ApolloQueryResult; public previousData: undefined | TData; - public setResult(nextResult: ApolloQueryResult) { + public setResult( + nextResult: ApolloQueryResult, + forceUpdate: () => void + ) { const previousResult = this.result; if (previousResult && previousResult.data) { this.previousData = previousResult.data; @@ -577,7 +504,7 @@ class InternalState { this.result = nextResult; // Calling state.setResult always triggers an update, though some call sites // perform additional equality checks before committing to an update. - this.forceUpdate(); + forceUpdate(); this.handleErrorOrCompleted(nextResult, previousResult); } From ea77f601cdc3f2b4b6bb78c4e60762bb1e5e8746 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 27 May 2024 18:30:54 +0200 Subject: [PATCH 08/62] move more functions out --- src/react/hooks/useLazyQuery.ts | 8 +- src/react/hooks/useQuery.ts | 158 +++++++++++++++++--------------- 2 files changed, 87 insertions(+), 79 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index a6e3350a428..40a95d13605 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -18,6 +18,7 @@ import type { import type { InternalState } from "./useQuery.js"; import { createWatchQueryOptions, + toQueryResult, useInternalState, useQueryWithInternalState, } from "./useQuery.js"; @@ -214,13 +215,14 @@ function executeQuery( }, error: () => { resolve( - internalState.toQueryResult( - internalState.observable.getCurrentResult() + toQueryResult( + internalState.observable.getCurrentResult(), + internalState ) ); }, complete: () => { - resolve(internalState.toQueryResult(result)); + resolve(toQueryResult(result, internalState)); }, }); }); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index c2fdc84c186..3193117ec82 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -172,7 +172,7 @@ export function useQueryWithInternalState< ) { // If SSR has been explicitly disabled, and this function has been called // on the server side, return the default loading state. - internalState.result = internalState.ssrDisabledResult; + internalState.result = ssrDisabledResult; } else if ( internalState.queryHookOptions.skip || internalState.watchQueryOptions.fetchPolicy === "standby" @@ -187,10 +187,10 @@ export function useQueryWithInternalState< // previously received data is all of a sudden removed. Unfortunately, // changing this is breaking, so we'll have to wait until Apollo Client 4.0 // to address this. - internalState.result = internalState.skipStandbyResult; + internalState.result = skipStandbyResult; } else if ( - internalState.result === internalState.ssrDisabledResult || - internalState.result === internalState.skipStandbyResult + internalState.result === ssrDisabledResult || + internalState.result === skipStandbyResult ) { internalState.result = void 0; } @@ -314,9 +314,9 @@ export function useQueryWithInternalState< ); // TODO Remove this method when we remove support for options.partialRefetch. - internalState.unsafeHandlePartialRefetch(result); + unsafeHandlePartialRefetch(result, internalState); - return internalState.toQueryResult(result); + return toQueryResult(result, internalState); } export function useInternalState( @@ -454,20 +454,6 @@ class InternalState { return toMerge.reduce(mergeOptions) as WatchQueryOptions; } - public ssrDisabledResult = maybeDeepFreeze({ - loading: true, - data: void 0 as unknown as TData, - error: void 0, - networkStatus: NetworkStatus.loading, - }); - - public skipStandbyResult = maybeDeepFreeze({ - loading: false, - data: void 0 as unknown as TData, - error: void 0, - networkStatus: NetworkStatus.ready, - }); - getDefaultFetchPolicy(): WatchQueryFetchPolicy { return ( this.queryHookOptions.defaultOptions?.fetchPolicy || @@ -513,7 +499,7 @@ class InternalState { previousResult?: ApolloQueryResult ) { if (!result.loading) { - const error = this.toApolloError(result); + const error = toApolloError(result); // wait a tick in case we are in the middle of rendering a component Promise.resolve() @@ -534,14 +520,6 @@ class InternalState { } } - public toApolloError( - result: ApolloQueryResult - ): ApolloError | undefined { - return isNonEmptyArray(result.errors) ? - new ApolloError({ graphQLErrors: result.errors }) - : result.error; - } - public getCurrentResult(): ApolloQueryResult { // Using this.result as a cache ensures getCurrentResult continues returning // the same (===) result object, unless state.setResult has been called, or @@ -561,57 +539,85 @@ class InternalState { ApolloQueryResult, QueryResult >(); +} - toQueryResult( - result: ApolloQueryResult - ): QueryResult { - let queryResult = this.toQueryResultCache.get(result); - if (queryResult) return queryResult; - - const { data, partial, ...resultWithoutPartial } = result; - this.toQueryResultCache.set( - result, - (queryResult = { - data, // Ensure always defined, even if result.data is missing. - ...resultWithoutPartial, - ...this.obsQueryFields, - client: this.client, - observable: this.observable, - variables: this.observable.variables, - called: !this.queryHookOptions.skip, - previousData: this.previousData, - }) - ); +function toApolloError( + result: ApolloQueryResult +): ApolloError | undefined { + return isNonEmptyArray(result.errors) ? + new ApolloError({ graphQLErrors: result.errors }) + : result.error; +} - if (!queryResult.error && isNonEmptyArray(result.errors)) { - // Until a set naming convention for networkError and graphQLErrors is - // decided upon, we map errors (graphQLErrors) to the error options. - // TODO: Is it possible for both result.error and result.errors to be - // defined here? - queryResult.error = new ApolloError({ graphQLErrors: result.errors }); - } +export function toQueryResult( + result: ApolloQueryResult, + internalState: InternalState +): QueryResult { + let queryResult = internalState.toQueryResultCache.get(result); + if (queryResult) return queryResult; + + const { data, partial, ...resultWithoutPartial } = result; + internalState.toQueryResultCache.set( + result, + (queryResult = { + data, // Ensure always defined, even if result.data is missing. + ...resultWithoutPartial, + ...internalState.obsQueryFields, + client: internalState.client, + observable: internalState.observable, + variables: internalState.observable.variables, + called: !internalState.queryHookOptions.skip, + previousData: internalState.previousData, + }) + ); - return queryResult; + if (!queryResult.error && isNonEmptyArray(result.errors)) { + // Until a set naming convention for networkError and graphQLErrors is + // decided upon, we map errors (graphQLErrors) to the error options. + // TODO: Is it possible for both result.error and result.errors to be + // defined here? + queryResult.error = new ApolloError({ graphQLErrors: result.errors }); } - public unsafeHandlePartialRefetch(result: ApolloQueryResult) { - // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION - // - // TODO: This code should be removed when the partialRefetch option is - // removed. I was unable to get this hook to behave reasonably in certain - // edge cases when this block was put in an effect. - if ( - result.partial && - this.queryHookOptions.partialRefetch && - !result.loading && - (!result.data || Object.keys(result.data).length === 0) && - this.observable.options.fetchPolicy !== "cache-only" - ) { - Object.assign(result, { - loading: true, - networkStatus: NetworkStatus.refetch, - }); - this.observable.refetch(); - } + return queryResult; +} +function unsafeHandlePartialRefetch< + TData, + TVariables extends OperationVariables, +>( + result: ApolloQueryResult, + internalState: InternalState +) { + // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION + // + // TODO: This code should be removed when the partialRefetch option is + // removed. I was unable to get this hook to behave reasonably in certain + // edge cases when this block was put in an effect. + if ( + result.partial && + internalState.queryHookOptions.partialRefetch && + !result.loading && + (!result.data || Object.keys(result.data).length === 0) && + internalState.observable.options.fetchPolicy !== "cache-only" + ) { + Object.assign(result, { + loading: true, + networkStatus: NetworkStatus.refetch, + }); + internalState.observable.refetch(); } } + +const ssrDisabledResult = maybeDeepFreeze({ + loading: true, + data: void 0 as any, + error: void 0, + networkStatus: NetworkStatus.loading, +}); + +const skipStandbyResult = maybeDeepFreeze({ + loading: false, + data: void 0 as any, + error: void 0, + networkStatus: NetworkStatus.ready, +}); From fde4c4d5972a057eb52953810dca224cf1d74245 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 28 May 2024 16:06:03 +0200 Subject: [PATCH 09/62] move `unsafeHandlePartialRefetch` into `setResult` --- src/react/hooks/useQuery.ts | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 3193117ec82..614c2d987c4 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -313,9 +313,6 @@ export function useQueryWithInternalState< () => internalState.getCurrentResult() ); - // TODO Remove this method when we remove support for options.partialRefetch. - unsafeHandlePartialRefetch(result, internalState); - return toQueryResult(result, internalState); } @@ -487,7 +484,7 @@ class InternalState { if (previousResult && previousResult.data) { this.previousData = previousResult.data; } - this.result = nextResult; + this.result = unsafeHandlePartialRefetch(nextResult, this); // Calling state.setResult always triggers an update, though some call sites // perform additional equality checks before committing to an update. forceUpdate(); @@ -525,11 +522,11 @@ class InternalState { // the same (===) result object, unless state.setResult has been called, or // we're doing server rendering and therefore override the result below. if (!this.result) { - this.handleErrorOrCompleted( - (this.result = this.observable.getCurrentResult()) - ); + // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION + // this could call unsafeHandlePartialRefetch + this.setResult(this.observable.getCurrentResult(), () => {}); } - return this.result; + return this.result!; } // This cache allows the referential stability of this.result (as returned by @@ -581,15 +578,14 @@ export function toQueryResult( return queryResult; } + function unsafeHandlePartialRefetch< TData, TVariables extends OperationVariables, >( result: ApolloQueryResult, internalState: InternalState -) { - // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION - // +): ApolloQueryResult { // TODO: This code should be removed when the partialRefetch option is // removed. I was unable to get this hook to behave reasonably in certain // edge cases when this block was put in an effect. @@ -600,12 +596,14 @@ function unsafeHandlePartialRefetch< (!result.data || Object.keys(result.data).length === 0) && internalState.observable.options.fetchPolicy !== "cache-only" ) { - Object.assign(result, { + internalState.observable.refetch(); + return { + ...result, loading: true, networkStatus: NetworkStatus.refetch, - }); - internalState.observable.refetch(); + }; } + return result; } const ssrDisabledResult = maybeDeepFreeze({ From e1b7ed789815ec866044cbecd397f6c7a7ff9038 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 May 2024 14:26:19 +0200 Subject: [PATCH 10/62] remove `toQueryResultCache`, save transformed result in `internalState.result` --- src/react/hooks/useQuery.ts | 109 +++++++++++++++++------------------- 1 file changed, 52 insertions(+), 57 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 614c2d987c4..974b934349c 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -30,7 +30,6 @@ import type { import { DocumentType, verifyDocumentType } from "../parser/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { - canUseWeakMap, compact, isNonEmptyArray, maybeDeepFreeze, @@ -41,6 +40,12 @@ const { prototype: { hasOwnProperty }, } = Object; +const originalResult = Symbol(); +interface InternalQueryResult + extends QueryResult { + [originalResult]: ApolloQueryResult; +} + /** * A hook for executing queries in an Apollo application. * @@ -165,6 +170,28 @@ export function useQueryWithInternalState< options.onCompleted || InternalState.prototype.onCompleted; internalState.onError = options.onError || InternalState.prototype.onError; + // See if there is an existing observable that was used to fetch the same + // data and if so, use it instead since it will contain the proper queryId + // to fetch the result set. This is used during SSR. + const obsQuery = (internalState.observable = + (renderPromises && + renderPromises.getSSRObservable(internalState.watchQueryOptions)) || + internalState.observable || // Reuse this.observable if possible (and not SSR) + internalState.client.watchQuery(internalState.getObsQueryOptions())); + + internalState.obsQueryFields = React.useMemo( + () => ({ + refetch: obsQuery.refetch.bind(obsQuery), + reobserve: obsQuery.reobserve.bind(obsQuery), + fetchMore: obsQuery.fetchMore.bind(obsQuery), + updateQuery: obsQuery.updateQuery.bind(obsQuery), + startPolling: obsQuery.startPolling.bind(obsQuery), + stopPolling: obsQuery.stopPolling.bind(obsQuery), + subscribeToMore: obsQuery.subscribeToMore.bind(obsQuery), + }), + [obsQuery] + ); + if ( (renderPromises || internalState.client.disableNetworkFetches) && internalState.queryHookOptions.ssr === false && @@ -172,7 +199,7 @@ export function useQueryWithInternalState< ) { // If SSR has been explicitly disabled, and this function has been called // on the server side, return the default loading state. - internalState.result = ssrDisabledResult; + internalState.result = toQueryResult(ssrDisabledResult, internalState); } else if ( internalState.queryHookOptions.skip || internalState.watchQueryOptions.fetchPolicy === "standby" @@ -187,36 +214,14 @@ export function useQueryWithInternalState< // previously received data is all of a sudden removed. Unfortunately, // changing this is breaking, so we'll have to wait until Apollo Client 4.0 // to address this. - internalState.result = skipStandbyResult; + internalState.result = toQueryResult(skipStandbyResult, internalState); } else if ( - internalState.result === ssrDisabledResult || - internalState.result === skipStandbyResult + internalState.result?.[originalResult] === ssrDisabledResult || + internalState.result?.[originalResult] === skipStandbyResult ) { internalState.result = void 0; } - // See if there is an existing observable that was used to fetch the same - // data and if so, use it instead since it will contain the proper queryId - // to fetch the result set. This is used during SSR. - const obsQuery = (internalState.observable = - (renderPromises && - renderPromises.getSSRObservable(internalState.watchQueryOptions)) || - internalState.observable || // Reuse this.observable if possible (and not SSR) - internalState.client.watchQuery(internalState.getObsQueryOptions())); - - internalState.obsQueryFields = React.useMemo( - () => ({ - refetch: obsQuery.refetch.bind(obsQuery), - reobserve: obsQuery.reobserve.bind(obsQuery), - fetchMore: obsQuery.fetchMore.bind(obsQuery), - updateQuery: obsQuery.updateQuery.bind(obsQuery), - startPolling: obsQuery.startPolling.bind(obsQuery), - stopPolling: obsQuery.stopPolling.bind(obsQuery), - subscribeToMore: obsQuery.subscribeToMore.bind(obsQuery), - }), - [obsQuery] - ); - const ssrAllowed = !( internalState.queryHookOptions.ssr === false || internalState.queryHookOptions.skip @@ -313,7 +318,7 @@ export function useQueryWithInternalState< () => internalState.getCurrentResult() ); - return toQueryResult(result, internalState); + return result; } export function useInternalState( @@ -473,7 +478,7 @@ class InternalState { // These members are populated by getCurrentResult and setResult, and it's // okay/normal for them to be initially undefined. - public result: undefined | ApolloQueryResult; + public result: undefined | InternalQueryResult; public previousData: undefined | TData; public setResult( @@ -484,11 +489,14 @@ class InternalState { if (previousResult && previousResult.data) { this.previousData = previousResult.data; } - this.result = unsafeHandlePartialRefetch(nextResult, this); + this.result = toQueryResult( + unsafeHandlePartialRefetch(nextResult, this), + this + ); // Calling state.setResult always triggers an update, though some call sites // perform additional equality checks before committing to an update. forceUpdate(); - this.handleErrorOrCompleted(nextResult, previousResult); + this.handleErrorOrCompleted(nextResult, previousResult?.[originalResult]); } public handleErrorOrCompleted( @@ -517,7 +525,7 @@ class InternalState { } } - public getCurrentResult(): ApolloQueryResult { + public getCurrentResult(): QueryResult { // Using this.result as a cache ensures getCurrentResult continues returning // the same (===) result object, unless state.setResult has been called, or // we're doing server rendering and therefore override the result below. @@ -528,14 +536,6 @@ class InternalState { } return this.result!; } - - // This cache allows the referential stability of this.result (as returned by - // getCurrentResult) to translate into referential stability of the resulting - // QueryResult object returned by toQueryResult. - public toQueryResultCache = new (canUseWeakMap ? WeakMap : Map)< - ApolloQueryResult, - QueryResult - >(); } function toApolloError( @@ -549,24 +549,19 @@ function toApolloError( export function toQueryResult( result: ApolloQueryResult, internalState: InternalState -): QueryResult { - let queryResult = internalState.toQueryResultCache.get(result); - if (queryResult) return queryResult; - +): InternalQueryResult { const { data, partial, ...resultWithoutPartial } = result; - internalState.toQueryResultCache.set( - result, - (queryResult = { - data, // Ensure always defined, even if result.data is missing. - ...resultWithoutPartial, - ...internalState.obsQueryFields, - client: internalState.client, - observable: internalState.observable, - variables: internalState.observable.variables, - called: !internalState.queryHookOptions.skip, - previousData: internalState.previousData, - }) - ); + const queryResult: InternalQueryResult = { + [originalResult]: result, + data, // Ensure always defined, even if result.data is missing. + ...resultWithoutPartial, + ...internalState.obsQueryFields, + client: internalState.client, + observable: internalState.observable, + variables: internalState.observable.variables, + called: !internalState.queryHookOptions.skip, + previousData: internalState.previousData, + }; if (!queryResult.error && isNonEmptyArray(result.errors)) { // Until a set naming convention for networkError and graphQLErrors is From 9c865ca1a890d2b6a36ab1c79dcda02df42fcd54 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 May 2024 15:37:29 +0200 Subject: [PATCH 11/62] fixup test --- src/react/hooks/useQuery.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 974b934349c..a0e328f9bce 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -552,7 +552,6 @@ export function toQueryResult( ): InternalQueryResult { const { data, partial, ...resultWithoutPartial } = result; const queryResult: InternalQueryResult = { - [originalResult]: result, data, // Ensure always defined, even if result.data is missing. ...resultWithoutPartial, ...internalState.obsQueryFields, @@ -561,7 +560,12 @@ export function toQueryResult( variables: internalState.observable.variables, called: !internalState.queryHookOptions.skip, previousData: internalState.previousData, - }; + } satisfies QueryResult as InternalQueryResult< + TData, + TVariables + >; + // non-enumerable property to hold the original result, for referential equality checks + Object.defineProperty(queryResult, originalResult, { value: result }); if (!queryResult.error && isNonEmptyArray(result.errors)) { // Until a set naming convention for networkError and graphQLErrors is From b5e16431494ff97109039cc17b359cee3fb117f9 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 May 2024 16:08:57 +0200 Subject: [PATCH 12/62] move `getDefaultFetchPolicy` out --- src/react/hooks/useLazyQuery.ts | 6 +++++- src/react/hooks/useQuery.ts | 33 +++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 40a95d13605..0874af6ac32 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -18,6 +18,7 @@ import type { import type { InternalState } from "./useQuery.js"; import { createWatchQueryOptions, + getDefaultFetchPolicy, toQueryResult, useInternalState, useQueryWithInternalState, @@ -103,7 +104,10 @@ export function useLazyQuery< const initialFetchPolicy = useQueryResult.observable.options.initialFetchPolicy || - internalState.getDefaultFetchPolicy(); + getDefaultFetchPolicy( + internalState.queryHookOptions.defaultOptions, + internalState.client.defaultOptions + ); const { obsQueryFields } = internalState; const forceUpdateState = React.useReducer((tick) => tick + 1, 0)[1]; diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index a0e328f9bce..6a1c8e701f3 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -5,6 +5,7 @@ import { useSyncExternalStore } from "./useSyncExternalStore.js"; import { equal } from "@wry/equality"; import type { + DefaultOptions, OperationVariables, WatchQueryFetchPolicy, } from "../../core/index.js"; @@ -385,7 +386,10 @@ export function createWatchQueryOptions< if (skip) { const { - fetchPolicy = internalState.getDefaultFetchPolicy(), + fetchPolicy = getDefaultFetchPolicy( + internalState.queryHookOptions.defaultOptions, + internalState.client.defaultOptions + ), initialFetchPolicy = fetchPolicy, } = watchQueryOptions; @@ -399,7 +403,10 @@ export function createWatchQueryOptions< } else if (!watchQueryOptions.fetchPolicy) { watchQueryOptions.fetchPolicy = internalState.observable?.options.initialFetchPolicy || - internalState.getDefaultFetchPolicy(); + getDefaultFetchPolicy( + internalState.queryHookOptions.defaultOptions, + internalState.client.defaultOptions + ); } return watchQueryOptions; @@ -456,14 +463,6 @@ class InternalState { return toMerge.reduce(mergeOptions) as WatchQueryOptions; } - getDefaultFetchPolicy(): WatchQueryFetchPolicy { - return ( - this.queryHookOptions.defaultOptions?.fetchPolicy || - this.client.defaultOptions.watchQuery?.fetchPolicy || - "cache-first" - ); - } - // Defining these methods as no-ops on the prototype allows us to call // state.onCompleted and/or state.onError without worrying about whether a // callback was provided. @@ -538,6 +537,20 @@ class InternalState { } } +export function getDefaultFetchPolicy< + TData, + TVariables extends OperationVariables, +>( + queryHookDefaultOptions?: Partial>, + clientDefaultOptions?: DefaultOptions +): WatchQueryFetchPolicy { + return ( + queryHookDefaultOptions?.fetchPolicy || + clientDefaultOptions?.watchQuery?.fetchPolicy || + "cache-first" + ); +} + function toApolloError( result: ApolloQueryResult ): ApolloError | undefined { From b4c8bf91bc2ea5830c3a3e24dce52009fe0a6fcf Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 May 2024 16:33:19 +0200 Subject: [PATCH 13/62] move more functions out --- src/react/hooks/useQuery.ts | 80 ++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 6a1c8e701f3..4d74a746f1a 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -315,8 +315,8 @@ export function useQueryWithInternalState< ] ), - () => internalState.getCurrentResult(), - () => internalState.getCurrentResult() + () => getCurrentResult(internalState), + () => getCurrentResult(internalState) ); return result; @@ -495,46 +495,52 @@ class InternalState { // Calling state.setResult always triggers an update, though some call sites // perform additional equality checks before committing to an update. forceUpdate(); - this.handleErrorOrCompleted(nextResult, previousResult?.[originalResult]); + handleErrorOrCompleted(nextResult, previousResult?.[originalResult], this); } +} - public handleErrorOrCompleted( - result: ApolloQueryResult, - previousResult?: ApolloQueryResult - ) { - if (!result.loading) { - const error = toApolloError(result); - - // wait a tick in case we are in the middle of rendering a component - Promise.resolve() - .then(() => { - if (error) { - this.onError(error); - } else if ( - result.data && - previousResult?.networkStatus !== result.networkStatus && - result.networkStatus === NetworkStatus.ready - ) { - this.onCompleted(result.data); - } - }) - .catch((error) => { - invariant.warn(error); - }); - } +function handleErrorOrCompleted( + result: ApolloQueryResult, + previousResult: ApolloQueryResult | undefined, + internalState: InternalState +) { + if (!result.loading) { + const error = toApolloError(result); + + // wait a tick in case we are in the middle of rendering a component + Promise.resolve() + .then(() => { + if (error) { + internalState.onError(error); + } else if ( + result.data && + previousResult?.networkStatus !== result.networkStatus && + result.networkStatus === NetworkStatus.ready + ) { + internalState.onCompleted(result.data); + } + }) + .catch((error) => { + invariant.warn(error); + }); } +} - public getCurrentResult(): QueryResult { - // Using this.result as a cache ensures getCurrentResult continues returning - // the same (===) result object, unless state.setResult has been called, or - // we're doing server rendering and therefore override the result below. - if (!this.result) { - // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION - // this could call unsafeHandlePartialRefetch - this.setResult(this.observable.getCurrentResult(), () => {}); - } - return this.result!; +function getCurrentResult( + internalState: InternalState +): QueryResult { + // Using this.result as a cache ensures getCurrentResult continues returning + // the same (===) result object, unless state.setResult has been called, or + // we're doing server rendering and therefore override the result below. + if (!internalState.result) { + // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION + // this could call unsafeHandlePartialRefetch + internalState.setResult( + internalState.observable.getCurrentResult(), + () => {} + ); } + return internalState.result!; } export function getDefaultFetchPolicy< From 12e19f99254d7cb39f0ee185809d28b4ca1270d3 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 May 2024 16:40:08 +0200 Subject: [PATCH 14/62] moved all class methods out --- src/react/hooks/useLazyQuery.ts | 3 +- src/react/hooks/useQuery.ts | 118 ++++++++++++++++++-------------- 2 files changed, 67 insertions(+), 54 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 0874af6ac32..d13d7596664 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -19,6 +19,7 @@ import type { InternalState } from "./useQuery.js"; import { createWatchQueryOptions, getDefaultFetchPolicy, + getObsQueryOptions, toQueryResult, useInternalState, useQueryWithInternalState, @@ -195,7 +196,7 @@ function executeQuery( ); const concast = internalState.observable.reobserveAsConcast( - internalState.getObsQueryOptions() + getObsQueryOptions(internalState) ); // Make sure getCurrentResult returns a fresh ApolloQueryResult, diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 4d74a746f1a..1140314db41 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -150,7 +150,7 @@ export function useQueryWithInternalState< // subscriptions, though it does feel less than ideal that reobserve // (potentially) kicks off a network request (for example, when the // variables have changed), which is technically a side-effect. - internalState.observable.reobserve(internalState.getObsQueryOptions()); + internalState.observable.reobserve(getObsQueryOptions(internalState)); // Make sure getCurrentResult returns a fresh ApolloQueryResult, // but save the current data as this.previousData, just like setResult @@ -178,7 +178,7 @@ export function useQueryWithInternalState< (renderPromises && renderPromises.getSSRObservable(internalState.watchQueryOptions)) || internalState.observable || // Reuse this.observable if possible (and not SSR) - internalState.client.watchQuery(internalState.getObsQueryOptions())); + internalState.client.watchQuery(getObsQueryOptions(internalState))); internalState.obsQueryFields = React.useMemo( () => ({ @@ -260,7 +260,7 @@ export function useQueryWithInternalState< return; } - internalState.setResult(result, handleStoreChange); + setResult(result, handleStoreChange, internalState); }; const onError = (error: Error) => { @@ -278,14 +278,15 @@ export function useQueryWithInternalState< (previousResult && previousResult.loading) || !equal(error, previousResult.error) ) { - internalState.setResult( + setResult( { data: (previousResult && previousResult.data) as TData, error: error as ApolloError, loading: false, networkStatus: NetworkStatus.error, }, - handleStoreChange + handleStoreChange, + internalState ); } }; @@ -433,36 +434,6 @@ class InternalState { public queryHookOptions!: QueryHookOptions; public watchQueryOptions!: WatchQueryOptions; - public getObsQueryOptions(): WatchQueryOptions { - const toMerge: Array>> = []; - - const globalDefaults = this.client.defaultOptions.watchQuery; - if (globalDefaults) toMerge.push(globalDefaults); - - if (this.queryHookOptions.defaultOptions) { - toMerge.push(this.queryHookOptions.defaultOptions); - } - - // We use compact rather than mergeOptions for this part of the merge, - // because we want watchQueryOptions.variables (if defined) to replace - // this.observable.options.variables whole. This replacement allows - // removing variables by removing them from the variables input to - // useQuery. If the variables were always merged together (rather than - // replaced), there would be no way to remove existing variables. - // However, the variables from options.defaultOptions and globalDefaults - // (if provided) should be merged, to ensure individual defaulted - // variables always have values, if not otherwise defined in - // observable.options or watchQueryOptions. - toMerge.push( - compact( - this.observable && this.observable.options, - this.watchQueryOptions - ) - ); - - return toMerge.reduce(mergeOptions) as WatchQueryOptions; - } - // Defining these methods as no-ops on the prototype allows us to call // state.onCompleted and/or state.onError without worrying about whether a // callback was provided. @@ -479,24 +450,64 @@ class InternalState { // okay/normal for them to be initially undefined. public result: undefined | InternalQueryResult; public previousData: undefined | TData; +} - public setResult( - nextResult: ApolloQueryResult, - forceUpdate: () => void - ) { - const previousResult = this.result; - if (previousResult && previousResult.data) { - this.previousData = previousResult.data; - } - this.result = toQueryResult( - unsafeHandlePartialRefetch(nextResult, this), - this - ); - // Calling state.setResult always triggers an update, though some call sites - // perform additional equality checks before committing to an update. - forceUpdate(); - handleErrorOrCompleted(nextResult, previousResult?.[originalResult], this); +export function getObsQueryOptions< + TData, + TVariables extends OperationVariables, +>( + internalState: InternalState +): WatchQueryOptions { + const toMerge: Array>> = []; + + const globalDefaults = internalState.client.defaultOptions.watchQuery; + if (globalDefaults) toMerge.push(globalDefaults); + + if (internalState.queryHookOptions.defaultOptions) { + toMerge.push(internalState.queryHookOptions.defaultOptions); + } + + // We use compact rather than mergeOptions for this part of the merge, + // because we want watchQueryOptions.variables (if defined) to replace + // this.observable.options.variables whole. This replacement allows + // removing variables by removing them from the variables input to + // useQuery. If the variables were always merged together (rather than + // replaced), there would be no way to remove existing variables. + // However, the variables from options.defaultOptions and globalDefaults + // (if provided) should be merged, to ensure individual defaulted + // variables always have values, if not otherwise defined in + // observable.options or watchQueryOptions. + toMerge.push( + compact( + internalState.observable && internalState.observable.options, + internalState.watchQueryOptions + ) + ); + + return toMerge.reduce(mergeOptions) as WatchQueryOptions; +} + +function setResult( + nextResult: ApolloQueryResult, + forceUpdate: () => void, + internalState: InternalState +) { + const previousResult = internalState.result; + if (previousResult && previousResult.data) { + internalState.previousData = previousResult.data; } + internalState.result = toQueryResult( + unsafeHandlePartialRefetch(nextResult, internalState), + internalState + ); + // Calling state.setResult always triggers an update, though some call sites + // perform additional equality checks before committing to an update. + forceUpdate(); + handleErrorOrCompleted( + nextResult, + previousResult?.[originalResult], + internalState + ); } function handleErrorOrCompleted( @@ -535,9 +546,10 @@ function getCurrentResult( if (!internalState.result) { // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION // this could call unsafeHandlePartialRefetch - internalState.setResult( + setResult( internalState.observable.getCurrentResult(), - () => {} + () => {}, + internalState ); } return internalState.result!; From 24e4491876e44bc11d1335678c699653c88d9ba4 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 May 2024 17:00:29 +0200 Subject: [PATCH 15/62] replace class with single mutable object --- src/react/hooks/useQuery.ts | 85 ++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 1140314db41..9c4cede7269 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -47,6 +47,29 @@ interface InternalQueryResult [originalResult]: ApolloQueryResult; } +const noop = () => {}; + +export interface InternalState { + readonly client: ReturnType; + query: DocumentNode | TypedDocumentNode; + + queryHookOptions: QueryHookOptions; + watchQueryOptions: WatchQueryOptions; + + // Defining these methods as no-ops on the prototype allows us to call + // state.onCompleted and/or state.onError without worrying about whether a + // callback was provided. + onCompleted(data: TData): void; + onError(error: ApolloError): void; + + observable: ObservableQuery; + obsQueryFields: Omit, "variables">; + // These members are populated by getCurrentResult and setResult, and it's + // okay/normal for them to be initially undefined. + result: undefined | InternalQueryResult; + previousData: undefined | TData; +} + /** * A hook for executing queries in an Apollo application. * @@ -167,9 +190,8 @@ export function useQueryWithInternalState< // Like the forceUpdate method, the versions of these methods inherited from // InternalState.prototype are empty no-ops, but we can override them on the // base state object (without modifying the prototype). - internalState.onCompleted = - options.onCompleted || InternalState.prototype.onCompleted; - internalState.onError = options.onError || InternalState.prototype.onError; + internalState.onCompleted = options.onCompleted || noop; + internalState.onError = options.onError || noop; // See if there is an existing observable that was used to fetch the same // data and if so, use it instead since it will contain the proper queryId @@ -328,7 +350,23 @@ export function useInternalState( query: DocumentNode | TypedDocumentNode ): InternalState { function createInternalState(previous?: InternalState) { - return new InternalState(client, query, previous); + verifyDocumentType(query, DocumentType.Query); + + // Reuse previousData from previous InternalState (if any) to provide + // continuity of previousData even if/when the query or client changes. + const previousResult = previous && previous.result; + const previousData = previousResult && previousResult.data; + const internalState: Partial> = { + client, + query, + onCompleted: noop, + onError: noop, + }; + if (previousData) { + internalState.previousData = previousData; + } + + return internalState as InternalState; } let [state, updateState] = React.useState(createInternalState); @@ -413,45 +451,6 @@ export function createWatchQueryOptions< return watchQueryOptions; } -export { type InternalState }; -class InternalState { - constructor( - public readonly client: ReturnType, - public query: DocumentNode | TypedDocumentNode, - previous?: InternalState - ) { - verifyDocumentType(query, DocumentType.Query); - - // Reuse previousData from previous InternalState (if any) to provide - // continuity of previousData even if/when the query or client changes. - const previousResult = previous && previous.result; - const previousData = previousResult && previousResult.data; - if (previousData) { - this.previousData = previousData; - } - } - - public queryHookOptions!: QueryHookOptions; - public watchQueryOptions!: WatchQueryOptions; - - // Defining these methods as no-ops on the prototype allows us to call - // state.onCompleted and/or state.onError without worrying about whether a - // callback was provided. - public onCompleted(data: TData) {} - public onError(error: ApolloError) {} - - public observable!: ObservableQuery; - public obsQueryFields!: Omit< - ObservableQueryFields, - "variables" - >; - - // These members are populated by getCurrentResult and setResult, and it's - // okay/normal for them to be initially undefined. - public result: undefined | InternalQueryResult; - public previousData: undefined | TData; -} - export function getObsQueryOptions< TData, TVariables extends OperationVariables, From e31d953b085c78e0d1ef5381a00a12d99099c8ee Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 May 2024 17:08:31 +0200 Subject: [PATCH 16/62] move callbacks into their own ref --- src/react/hooks/useQuery.ts | 70 +++++++++++++++++++++++-------------- 1 file changed, 43 insertions(+), 27 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 9c4cede7269..4683c749bd5 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -56,12 +56,6 @@ export interface InternalState { queryHookOptions: QueryHookOptions; watchQueryOptions: WatchQueryOptions; - // Defining these methods as no-ops on the prototype allows us to call - // state.onCompleted and/or state.onError without worrying about whether a - // callback was provided. - onCompleted(data: TData): void; - onError(error: ApolloError): void; - observable: ObservableQuery; obsQueryFields: Omit, "variables">; // These members are populated by getCurrentResult and setResult, and it's @@ -70,6 +64,14 @@ export interface InternalState { previousData: undefined | TData; } +interface Callbacks { + // Defining these methods as no-ops on the prototype allows us to call + // state.onCompleted and/or state.onError without worrying about whether a + // callback was provided. + onCompleted(data: TData): void; + onError(error: ApolloError): void; +} + /** * A hook for executing queries in an Apollo application. * @@ -184,14 +186,20 @@ export function useQueryWithInternalState< } } - // Make sure state.onCompleted and state.onError always reflect the latest - // options.onCompleted and options.onError callbacks provided to useQuery, - // since those functions are often recreated every time useQuery is called. - // Like the forceUpdate method, the versions of these methods inherited from - // InternalState.prototype are empty no-ops, but we can override them on the - // base state object (without modifying the prototype). - internalState.onCompleted = options.onCompleted || noop; - internalState.onError = options.onError || noop; + const _callbacks = { + onCompleted: options.onCompleted || noop, + onError: options.onError || noop, + }; + const callbackRef = React.useRef>(_callbacks); + React.useEffect(() => { + // Make sure state.onCompleted and state.onError always reflect the latest + // options.onCompleted and options.onError callbacks provided to useQuery, + // since those functions are often recreated every time useQuery is called. + // Like the forceUpdate method, the versions of these methods inherited from + // InternalState.prototype are empty no-ops, but we can override them on the + // base state object (without modifying the prototype). + callbackRef.current = _callbacks; + }); // See if there is an existing observable that was used to fetch the same // data and if so, use it instead since it will contain the proper queryId @@ -282,7 +290,12 @@ export function useQueryWithInternalState< return; } - setResult(result, handleStoreChange, internalState); + setResult( + result, + handleStoreChange, + internalState, + callbackRef.current + ); }; const onError = (error: Error) => { @@ -308,7 +321,8 @@ export function useQueryWithInternalState< networkStatus: NetworkStatus.error, }, handleStoreChange, - internalState + internalState, + callbackRef.current ); } }; @@ -338,8 +352,8 @@ export function useQueryWithInternalState< ] ), - () => getCurrentResult(internalState), - () => getCurrentResult(internalState) + () => getCurrentResult(internalState, callbackRef.current), + () => getCurrentResult(internalState, callbackRef.current) ); return result; @@ -359,8 +373,6 @@ export function useInternalState( const internalState: Partial> = { client, query, - onCompleted: noop, - onError: noop, }; if (previousData) { internalState.previousData = previousData; @@ -383,6 +395,7 @@ export function useInternalState( return state; } + // A function to massage options before passing them to ObservableQuery. export function createWatchQueryOptions< TData = any, @@ -489,7 +502,8 @@ export function getObsQueryOptions< function setResult( nextResult: ApolloQueryResult, forceUpdate: () => void, - internalState: InternalState + internalState: InternalState, + callbacks: Callbacks ) { const previousResult = internalState.result; if (previousResult && previousResult.data) { @@ -505,14 +519,14 @@ function setResult( handleErrorOrCompleted( nextResult, previousResult?.[originalResult], - internalState + callbacks ); } function handleErrorOrCompleted( result: ApolloQueryResult, previousResult: ApolloQueryResult | undefined, - internalState: InternalState + callbacks: Callbacks ) { if (!result.loading) { const error = toApolloError(result); @@ -521,13 +535,13 @@ function handleErrorOrCompleted( Promise.resolve() .then(() => { if (error) { - internalState.onError(error); + callbacks.onError(error); } else if ( result.data && previousResult?.networkStatus !== result.networkStatus && result.networkStatus === NetworkStatus.ready ) { - internalState.onCompleted(result.data); + callbacks.onCompleted(result.data); } }) .catch((error) => { @@ -537,7 +551,8 @@ function handleErrorOrCompleted( } function getCurrentResult( - internalState: InternalState + internalState: InternalState, + callbacks: Callbacks ): QueryResult { // Using this.result as a cache ensures getCurrentResult continues returning // the same (===) result object, unless state.setResult has been called, or @@ -548,7 +563,8 @@ function getCurrentResult( setResult( internalState.observable.getCurrentResult(), () => {}, - internalState + internalState, + callbacks ); } return internalState.result!; From 891e2113182542c46a09c6b17e605d24afcb2fdb Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 3 Jun 2024 15:40:32 +0200 Subject: [PATCH 17/62] move `obsQueryFields` out of `internalState` --- src/react/hooks/useLazyQuery.ts | 32 ++++++++++++++++++----------- src/react/hooks/useQuery.ts | 36 ++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index d13d7596664..c07c8ab34eb 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -21,10 +21,8 @@ import { getDefaultFetchPolicy, getObsQueryOptions, toQueryResult, - useInternalState, useQueryWithInternalState, } from "./useQuery.js"; -import { useApolloClient } from "./useApolloClient.js"; // The following methods, when called will execute the query, regardless of // whether the useLazyQuery execute function was called before. @@ -34,6 +32,7 @@ const EAGER_METHODS = [ "fetchMore", "updateQuery", "startPolling", + "stopPolling", "subscribeToMore", ] as const; @@ -93,12 +92,11 @@ export function useLazyQuery< optionsRef.current = options; queryRef.current = document; - const internalState = useInternalState( - useApolloClient(options && options.client), - document - ); - - const useQueryResult = useQueryWithInternalState(internalState, { + const { + internalState, + obsQueryFields, + result: useQueryResult, + } = useQueryWithInternalState(document, { ...merged, skip: !execOptionsRef.current, }); @@ -110,7 +108,6 @@ export function useLazyQuery< internalState.client.defaultOptions ); - const { obsQueryFields } = internalState; const forceUpdateState = React.useReducer((tick) => tick + 1, 0)[1]; // We use useMemo here to make sure the eager methods have a stable identity. const eagerMethods = React.useMemo(() => { @@ -128,7 +125,7 @@ export function useLazyQuery< }; } - return eagerMethods; + return eagerMethods as typeof obsQueryFields; }, [forceUpdateState, obsQueryFields]); const called = !!execOptionsRef.current; @@ -174,7 +171,16 @@ export function useLazyQuery< [eagerMethods, forceUpdateState, initialFetchPolicy, internalState] ); - return [execute, result]; + const executeRef = React.useRef(execute); + React.useLayoutEffect(() => { + executeRef.current = execute; + }); + + const stableExecute = React.useCallback( + (...args) => executeRef.current(...args), + [] + ); + return [stableExecute, result]; } function executeQuery( @@ -207,7 +213,9 @@ function executeQuery( internalState.result = void 0; forceUpdate(); - return new Promise>((resolve) => { + return new Promise< + Omit, (typeof EAGER_METHODS)[number]> + >((resolve) => { let result: ApolloQueryResult; // Subscribe to the concast independently of the ObservableQuery in case diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 4683c749bd5..de1b7e2515c 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -43,7 +43,10 @@ const { const originalResult = Symbol(); interface InternalQueryResult - extends QueryResult { + extends Omit< + QueryResult, + Exclude, "variables"> + > { [originalResult]: ApolloQueryResult; } @@ -57,7 +60,6 @@ export interface InternalState { watchQueryOptions: WatchQueryOptions; observable: ObservableQuery; - obsQueryFields: Omit, "variables">; // These members are populated by getCurrentResult and setResult, and it's // okay/normal for them to be initially undefined. result: undefined | InternalQueryResult; @@ -130,9 +132,10 @@ function _useQuery< query: DocumentNode | TypedDocumentNode, options: QueryHookOptions, NoInfer> ) { - return useQueryWithInternalState( - useInternalState(useApolloClient(options.client), query), - options + const { result, obsQueryFields } = useQueryWithInternalState(query, options); + return React.useMemo( + () => ({ ...result, ...obsQueryFields }), + [result, obsQueryFields] ); } @@ -140,9 +143,13 @@ export function useQueryWithInternalState< TData = any, TVariables extends OperationVariables = OperationVariables, >( - internalState: InternalState, + query: DocumentNode | TypedDocumentNode, options: QueryHookOptions, NoInfer> ) { + const internalState = useInternalState( + useApolloClient(options.client), + query + ); // The renderPromises field gets initialized here in the useQuery method, at // the beginning of everything (for a given component rendering, at least), // so we can safely use this.renderPromises in other/later InternalState @@ -210,7 +217,9 @@ export function useQueryWithInternalState< internalState.observable || // Reuse this.observable if possible (and not SSR) internalState.client.watchQuery(getObsQueryOptions(internalState))); - internalState.obsQueryFields = React.useMemo( + const obsQueryFields = React.useMemo< + Omit, "variables"> + >( () => ({ refetch: obsQuery.refetch.bind(obsQuery), reobserve: obsQuery.reobserve.bind(obsQuery), @@ -356,7 +365,7 @@ export function useQueryWithInternalState< () => getCurrentResult(internalState, callbackRef.current) ); - return result; + return { result, obsQueryFields, internalState }; } export function useInternalState( @@ -553,7 +562,7 @@ function handleErrorOrCompleted( function getCurrentResult( internalState: InternalState, callbacks: Callbacks -): QueryResult { +): InternalQueryResult { // Using this.result as a cache ensures getCurrentResult continues returning // the same (===) result object, unless state.setResult has been called, or // we're doing server rendering and therefore override the result below. @@ -600,16 +609,15 @@ export function toQueryResult( const queryResult: InternalQueryResult = { data, // Ensure always defined, even if result.data is missing. ...resultWithoutPartial, - ...internalState.obsQueryFields, client: internalState.client, observable: internalState.observable, variables: internalState.observable.variables, called: !internalState.queryHookOptions.skip, previousData: internalState.previousData, - } satisfies QueryResult as InternalQueryResult< - TData, - TVariables - >; + } satisfies Omit< + InternalQueryResult, + typeof originalResult + > as InternalQueryResult; // non-enumerable property to hold the original result, for referential equality checks Object.defineProperty(queryResult, originalResult, { value: result }); From ec1c7a92d23ee7c0f541c2944fcc1c88fe986705 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 4 Jun 2024 11:06:54 +0200 Subject: [PATCH 18/62] inline `useInternalState` --- src/react/hooks/useQuery.ts | 72 ++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 41 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index de1b7e2515c..caa2e9e69b9 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -146,10 +146,37 @@ export function useQueryWithInternalState< query: DocumentNode | TypedDocumentNode, options: QueryHookOptions, NoInfer> ) { - const internalState = useInternalState( - useApolloClient(options.client), - query - ); + const client = useApolloClient(options.client); + function createInternalState(previous?: InternalState) { + verifyDocumentType(query, DocumentType.Query); + + // Reuse previousData from previous InternalState (if any) to provide + // continuity of previousData even if/when the query or client changes. + const previousResult = previous && previous.result; + const previousData = previousResult && previousResult.data; + const internalState: Partial> = { + client, + query, + }; + if (previousData) { + internalState.previousData = previousData; + } + + return internalState as InternalState; + } + + let [internalState, updateState] = React.useState(createInternalState); + + if (client !== internalState.client || query !== internalState.query) { + // If the client or query have changed, we need to create a new InternalState. + // This will trigger a re-render with the new state, but it will also continue + // to run the current render function to completion. + // Since we sometimes trigger some side-effects in the render function, we + // re-assign `state` to the new state to ensure that those side-effects are + // triggered with the new state. + updateState((internalState = createInternalState(internalState))); + } + // The renderPromises field gets initialized here in the useQuery method, at // the beginning of everything (for a given component rendering, at least), // so we can safely use this.renderPromises in other/later InternalState @@ -368,43 +395,6 @@ export function useQueryWithInternalState< return { result, obsQueryFields, internalState }; } -export function useInternalState( - client: ApolloClient, - query: DocumentNode | TypedDocumentNode -): InternalState { - function createInternalState(previous?: InternalState) { - verifyDocumentType(query, DocumentType.Query); - - // Reuse previousData from previous InternalState (if any) to provide - // continuity of previousData even if/when the query or client changes. - const previousResult = previous && previous.result; - const previousData = previousResult && previousResult.data; - const internalState: Partial> = { - client, - query, - }; - if (previousData) { - internalState.previousData = previousData; - } - - return internalState as InternalState; - } - - let [state, updateState] = React.useState(createInternalState); - - if (client !== state.client || query !== state.query) { - // If the client or query have changed, we need to create a new InternalState. - // This will trigger a re-render with the new state, but it will also continue - // to run the current render function to completion. - // Since we sometimes trigger some side-effects in the render function, we - // re-assign `state` to the new state to ensure that those side-effects are - // triggered with the new state. - updateState((state = createInternalState(state))); - } - - return state; -} - // A function to massage options before passing them to ObservableQuery. export function createWatchQueryOptions< TData = any, From 79566a80825a5bcce7152d3b7fdacca4fd6a056f Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 4 Jun 2024 13:02:01 +0200 Subject: [PATCH 19/62] redactor away `internalState.queryHookOptions` --- src/react/hooks/useLazyQuery.ts | 15 ++++---- src/react/hooks/useQuery.ts | 64 ++++++++++++++++++--------------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index c07c8ab34eb..cce41ebf3cb 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -92,19 +92,20 @@ export function useLazyQuery< optionsRef.current = options; queryRef.current = document; + const queryHookOptions = { + ...merged, + skip: !execOptionsRef.current, + }; const { internalState, obsQueryFields, result: useQueryResult, - } = useQueryWithInternalState(document, { - ...merged, - skip: !execOptionsRef.current, - }); + } = useQueryWithInternalState(document, queryHookOptions); const initialFetchPolicy = useQueryResult.observable.options.initialFetchPolicy || getDefaultFetchPolicy( - internalState.queryHookOptions.defaultOptions, + queryHookOptions.defaultOptions, internalState.client.defaultOptions ); @@ -196,13 +197,13 @@ function executeQuery( } internalState.watchQueryOptions = createWatchQueryOptions( - (internalState.queryHookOptions = options), + options, internalState, hasRenderPromises ); const concast = internalState.observable.reobserveAsConcast( - getObsQueryOptions(internalState) + getObsQueryOptions(internalState, options) ); // Make sure getCurrentResult returns a fresh ApolloQueryResult, diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index caa2e9e69b9..6a4098f3116 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -13,7 +13,6 @@ import { mergeOptions } from "../../utilities/index.js"; import { getApolloContext } from "../context/index.js"; import { ApolloError } from "../../errors/index.js"; import type { - ApolloClient, ApolloQueryResult, ObservableQuery, DocumentNode, @@ -56,7 +55,6 @@ export interface InternalState { readonly client: ReturnType; query: DocumentNode | TypedDocumentNode; - queryHookOptions: QueryHookOptions; watchQueryOptions: WatchQueryOptions; observable: ObservableQuery; @@ -187,7 +185,7 @@ export function useQueryWithInternalState< const renderPromises = React.useContext(getApolloContext()).renderPromises; const watchQueryOptions = createWatchQueryOptions( - (internalState.queryHookOptions = options), + options, internalState, !!renderPromises ); @@ -209,7 +207,9 @@ export function useQueryWithInternalState< // subscriptions, though it does feel less than ideal that reobserve // (potentially) kicks off a network request (for example, when the // variables have changed), which is technically a side-effect. - internalState.observable.reobserve(getObsQueryOptions(internalState)); + internalState.observable.reobserve( + getObsQueryOptions(internalState, options) + ); // Make sure getCurrentResult returns a fresh ApolloQueryResult, // but save the current data as this.previousData, just like setResult @@ -242,7 +242,9 @@ export function useQueryWithInternalState< (renderPromises && renderPromises.getSSRObservable(internalState.watchQueryOptions)) || internalState.observable || // Reuse this.observable if possible (and not SSR) - internalState.client.watchQuery(getObsQueryOptions(internalState))); + internalState.client.watchQuery( + getObsQueryOptions(internalState, options) + )); const obsQueryFields = React.useMemo< Omit, "variables"> @@ -261,14 +263,14 @@ export function useQueryWithInternalState< if ( (renderPromises || internalState.client.disableNetworkFetches) && - internalState.queryHookOptions.ssr === false && - !internalState.queryHookOptions.skip + options.ssr === false && + !options.skip ) { // If SSR has been explicitly disabled, and this function has been called // on the server side, return the default loading state. internalState.result = toQueryResult(ssrDisabledResult, internalState); } else if ( - internalState.queryHookOptions.skip || + options.skip || internalState.watchQueryOptions.fetchPolicy === "standby" ) { // When skipping a query (ie. we're not querying for data but still want to @@ -289,10 +291,7 @@ export function useQueryWithInternalState< internalState.result = void 0; } - const ssrAllowed = !( - internalState.queryHookOptions.ssr === false || - internalState.queryHookOptions.skip - ); + const ssrAllowed = !(options.ssr === false || options.skip); if (renderPromises && ssrAllowed) { renderPromises.registerSSRObservable(obsQuery); @@ -303,6 +302,7 @@ export function useQueryWithInternalState< } } + const partialRefetch = options.partialRefetch; const result = useSyncExternalStore( React.useCallback( (handleStoreChange) => { @@ -330,7 +330,8 @@ export function useQueryWithInternalState< result, handleStoreChange, internalState, - callbackRef.current + callbackRef.current, + partialRefetch ); }; @@ -358,7 +359,8 @@ export function useQueryWithInternalState< }, handleStoreChange, internalState, - callbackRef.current + callbackRef.current, + partialRefetch ); } }; @@ -385,11 +387,12 @@ export function useQueryWithInternalState< obsQuery, renderPromises, internalState.client.disableNetworkFetches, + partialRefetch, ] ), - () => getCurrentResult(internalState, callbackRef.current), - () => getCurrentResult(internalState, callbackRef.current) + () => getCurrentResult(internalState, callbackRef.current, partialRefetch), + () => getCurrentResult(internalState, callbackRef.current, partialRefetch) ); return { result, obsQueryFields, internalState }; @@ -438,7 +441,7 @@ export function createWatchQueryOptions< if (skip) { const { fetchPolicy = getDefaultFetchPolicy( - internalState.queryHookOptions.defaultOptions, + defaultOptions, internalState.client.defaultOptions ), initialFetchPolicy = fetchPolicy, @@ -455,7 +458,7 @@ export function createWatchQueryOptions< watchQueryOptions.fetchPolicy = internalState.observable?.options.initialFetchPolicy || getDefaultFetchPolicy( - internalState.queryHookOptions.defaultOptions, + defaultOptions, internalState.client.defaultOptions ); } @@ -467,15 +470,16 @@ export function getObsQueryOptions< TData, TVariables extends OperationVariables, >( - internalState: InternalState + internalState: InternalState, + queryHookOptions: QueryHookOptions ): WatchQueryOptions { const toMerge: Array>> = []; const globalDefaults = internalState.client.defaultOptions.watchQuery; if (globalDefaults) toMerge.push(globalDefaults); - if (internalState.queryHookOptions.defaultOptions) { - toMerge.push(internalState.queryHookOptions.defaultOptions); + if (queryHookOptions.defaultOptions) { + toMerge.push(queryHookOptions.defaultOptions); } // We use compact rather than mergeOptions for this part of the merge, @@ -502,14 +506,15 @@ function setResult( nextResult: ApolloQueryResult, forceUpdate: () => void, internalState: InternalState, - callbacks: Callbacks + callbacks: Callbacks, + partialRefetch: boolean | undefined ) { const previousResult = internalState.result; if (previousResult && previousResult.data) { internalState.previousData = previousResult.data; } internalState.result = toQueryResult( - unsafeHandlePartialRefetch(nextResult, internalState), + unsafeHandlePartialRefetch(nextResult, internalState, partialRefetch), internalState ); // Calling state.setResult always triggers an update, though some call sites @@ -551,7 +556,8 @@ function handleErrorOrCompleted( function getCurrentResult( internalState: InternalState, - callbacks: Callbacks + callbacks: Callbacks, + partialRefetch: boolean | undefined ): InternalQueryResult { // Using this.result as a cache ensures getCurrentResult continues returning // the same (===) result object, unless state.setResult has been called, or @@ -563,7 +569,8 @@ function getCurrentResult( internalState.observable.getCurrentResult(), () => {}, internalState, - callbacks + callbacks, + partialRefetch ); } return internalState.result!; @@ -602,7 +609,7 @@ export function toQueryResult( client: internalState.client, observable: internalState.observable, variables: internalState.observable.variables, - called: !internalState.queryHookOptions.skip, + called: result !== ssrDisabledResult && result !== skipStandbyResult, previousData: internalState.previousData, } satisfies Omit< InternalQueryResult, @@ -627,14 +634,15 @@ function unsafeHandlePartialRefetch< TVariables extends OperationVariables, >( result: ApolloQueryResult, - internalState: InternalState + internalState: InternalState, + partialRefetch: boolean | undefined ): ApolloQueryResult { // TODO: This code should be removed when the partialRefetch option is // removed. I was unable to get this hook to behave reasonably in certain // edge cases when this block was put in an effect. if ( result.partial && - internalState.queryHookOptions.partialRefetch && + partialRefetch && !result.loading && (!result.data || Object.keys(result.data).length === 0) && internalState.observable.options.fetchPolicy !== "cache-only" From 873f24d0688a6ebf65f6a1e3e787b11f8124638d Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 4 Jun 2024 14:17:44 +0200 Subject: [PATCH 20/62] make function arguments more explicit --- src/react/hooks/useLazyQuery.ts | 13 +++-- src/react/hooks/useQuery.ts | 87 ++++++++++++++++++--------------- 2 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index cce41ebf3cb..1bf7c4007cd 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -197,13 +197,20 @@ function executeQuery( } internalState.watchQueryOptions = createWatchQueryOptions( + internalState.client, + internalState.query, options, - internalState, - hasRenderPromises + hasRenderPromises, + internalState.observable ); const concast = internalState.observable.reobserveAsConcast( - getObsQueryOptions(internalState, options) + getObsQueryOptions( + internalState.client, + options, + internalState.watchQueryOptions, + internalState.observable + ) ); // Make sure getCurrentResult returns a fresh ApolloQueryResult, diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 6a4098f3116..fb397bbe743 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -5,6 +5,7 @@ import { useSyncExternalStore } from "./useSyncExternalStore.js"; import { equal } from "@wry/equality"; import type { + ApolloClient, DefaultOptions, OperationVariables, WatchQueryFetchPolicy, @@ -184,11 +185,14 @@ export function useQueryWithInternalState< // rather than left uninitialized. const renderPromises = React.useContext(getApolloContext()).renderPromises; - const watchQueryOptions = createWatchQueryOptions( - options, - internalState, - !!renderPromises - ); + const watchQueryOptions: Readonly> = + createWatchQueryOptions( + internalState.client, + internalState.query, + options, + !!renderPromises, + internalState.observable + ); // Update this.watchQueryOptions, but only when they have changed, which // allows us to depend on the referential stability of @@ -208,7 +212,12 @@ export function useQueryWithInternalState< // (potentially) kicks off a network request (for example, when the // variables have changed), which is technically a side-effect. internalState.observable.reobserve( - getObsQueryOptions(internalState, options) + getObsQueryOptions( + internalState.client, + options, + internalState.watchQueryOptions, + internalState.observable + ) ); // Make sure getCurrentResult returns a fresh ApolloQueryResult, @@ -243,7 +252,12 @@ export function useQueryWithInternalState< renderPromises.getSSRObservable(internalState.watchQueryOptions)) || internalState.observable || // Reuse this.observable if possible (and not SSR) internalState.client.watchQuery( - getObsQueryOptions(internalState, options) + getObsQueryOptions( + internalState.client, + options, + internalState.watchQueryOptions, + undefined + ) )); const obsQueryFields = React.useMemo< @@ -403,6 +417,8 @@ export function createWatchQueryOptions< TData = any, TVariables extends OperationVariables = OperationVariables, >( + client: ApolloClient, + query: DocumentNode | TypedDocumentNode, { skip, ssr, @@ -414,14 +430,14 @@ export function createWatchQueryOptions< // query property that we add below. ...otherOptions }: QueryHookOptions = {}, - internalState: InternalState, - hasRenderPromises: boolean + hasRenderPromises: boolean, + observable: ObservableQuery | undefined ): WatchQueryOptions { // This Object.assign is safe because otherOptions is a fresh ...rest object // that did not exist until just now, so modifications are still allowed. const watchQueryOptions: WatchQueryOptions = Object.assign( otherOptions, - { query: internalState.query } + { query } ); if ( @@ -439,28 +455,18 @@ export function createWatchQueryOptions< } if (skip) { - const { - fetchPolicy = getDefaultFetchPolicy( - defaultOptions, - internalState.client.defaultOptions - ), - initialFetchPolicy = fetchPolicy, - } = watchQueryOptions; - // When skipping, we set watchQueryOptions.fetchPolicy initially to // "standby", but we also need/want to preserve the initial non-standby // fetchPolicy that would have been used if not skipping. - Object.assign(watchQueryOptions, { - initialFetchPolicy, - fetchPolicy: "standby", - }); + watchQueryOptions.initialFetchPolicy = + watchQueryOptions.initialFetchPolicy || + watchQueryOptions.fetchPolicy || + getDefaultFetchPolicy(defaultOptions, client.defaultOptions); + watchQueryOptions.fetchPolicy = "standby"; } else if (!watchQueryOptions.fetchPolicy) { watchQueryOptions.fetchPolicy = - internalState.observable?.options.initialFetchPolicy || - getDefaultFetchPolicy( - defaultOptions, - internalState.client.defaultOptions - ); + observable?.options.initialFetchPolicy || + getDefaultFetchPolicy(defaultOptions, client.defaultOptions); } return watchQueryOptions; @@ -470,12 +476,14 @@ export function getObsQueryOptions< TData, TVariables extends OperationVariables, >( - internalState: InternalState, - queryHookOptions: QueryHookOptions + client: ApolloClient, + queryHookOptions: QueryHookOptions, + watchQueryOptions: Partial>, + observable: ObservableQuery | undefined ): WatchQueryOptions { const toMerge: Array>> = []; - const globalDefaults = internalState.client.defaultOptions.watchQuery; + const globalDefaults = client.defaultOptions.watchQuery; if (globalDefaults) toMerge.push(globalDefaults); if (queryHookOptions.defaultOptions) { @@ -492,12 +500,7 @@ export function getObsQueryOptions< // (if provided) should be merged, to ensure individual defaulted // variables always have values, if not otherwise defined in // observable.options or watchQueryOptions. - toMerge.push( - compact( - internalState.observable && internalState.observable.options, - internalState.watchQueryOptions - ) - ); + toMerge.push(compact(observable && observable.options, watchQueryOptions)); return toMerge.reduce(mergeOptions) as WatchQueryOptions; } @@ -514,7 +517,11 @@ function setResult( internalState.previousData = previousResult.data; } internalState.result = toQueryResult( - unsafeHandlePartialRefetch(nextResult, internalState, partialRefetch), + unsafeHandlePartialRefetch( + nextResult, + internalState.observable, + partialRefetch + ), internalState ); // Calling state.setResult always triggers an update, though some call sites @@ -634,7 +641,7 @@ function unsafeHandlePartialRefetch< TVariables extends OperationVariables, >( result: ApolloQueryResult, - internalState: InternalState, + observable: ObservableQuery, partialRefetch: boolean | undefined ): ApolloQueryResult { // TODO: This code should be removed when the partialRefetch option is @@ -645,9 +652,9 @@ function unsafeHandlePartialRefetch< partialRefetch && !result.loading && (!result.data || Object.keys(result.data).length === 0) && - internalState.observable.options.fetchPolicy !== "cache-only" + observable.options.fetchPolicy !== "cache-only" ) { - internalState.observable.refetch(); + observable.refetch(); return { ...result, loading: true, From 8bb445c7ff6aac518b39b701ac73ede16ea70044 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 5 Jun 2024 12:43:40 +0200 Subject: [PATCH 21/62] replace `internalState.watchQueryOptions` with `observable[lastWatchOptions]` --- src/react/hooks/useLazyQuery.ts | 6 ++-- src/react/hooks/useQuery.ts | 64 ++++++++++++++++----------------- 2 files changed, 36 insertions(+), 34 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 1bf7c4007cd..d9107569e8f 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -20,6 +20,7 @@ import { createWatchQueryOptions, getDefaultFetchPolicy, getObsQueryOptions, + lastWatchOptions, toQueryResult, useQueryWithInternalState, } from "./useQuery.js"; @@ -196,7 +197,7 @@ function executeQuery( internalState.query = options.query; } - internalState.watchQueryOptions = createWatchQueryOptions( + const watchQueryOptions = createWatchQueryOptions( internalState.client, internalState.query, options, @@ -208,10 +209,11 @@ function executeQuery( getObsQueryOptions( internalState.client, options, - internalState.watchQueryOptions, + watchQueryOptions, internalState.observable ) ); + internalState.observable[lastWatchOptions] = watchQueryOptions; // Make sure getCurrentResult returns a fresh ApolloQueryResult, // but save the current data as this.previousData, just like setResult diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index fb397bbe743..3178029cbe4 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -51,14 +51,17 @@ interface InternalQueryResult } const noop = () => {}; +export const lastWatchOptions = Symbol(); +interface ObsQueryWithMeta + extends ObservableQuery { + [lastWatchOptions]?: WatchQueryOptions; +} export interface InternalState { readonly client: ReturnType; query: DocumentNode | TypedDocumentNode; - watchQueryOptions: WatchQueryOptions; - - observable: ObservableQuery; + observable: ObsQueryWithMeta; // These members are populated by getCurrentResult and setResult, and it's // okay/normal for them to be initially undefined. result: undefined | InternalQueryResult; @@ -194,15 +197,30 @@ export function useQueryWithInternalState< internalState.observable ); - // Update this.watchQueryOptions, but only when they have changed, which - // allows us to depend on the referential stability of - // this.watchQueryOptions elsewhere. - const currentWatchQueryOptions = internalState.watchQueryOptions; - - if (!equal(watchQueryOptions, currentWatchQueryOptions)) { - internalState.watchQueryOptions = watchQueryOptions; + // See if there is an existing observable that was used to fetch the same + // data and if so, use it instead since it will contain the proper queryId + // to fetch the result set. This is used during SSR. + const obsQuery: ObsQueryWithMeta = + (internalState.observable = + (renderPromises && renderPromises.getSSRObservable(watchQueryOptions)) || + internalState.observable || // Reuse this.observable if possible (and not SSR) + internalState.client.watchQuery( + getObsQueryOptions( + internalState.client, + options, + watchQueryOptions, + undefined + ) + )); - if (currentWatchQueryOptions && internalState.observable) { + // TODO: this part is not compatible with any rules of React, and there's + // no good way to rewrite it. + // it should be moved out into a separate hook that will not be optimized by the compiler + { + if ( + obsQuery[lastWatchOptions] && + !equal(obsQuery[lastWatchOptions], watchQueryOptions) + ) { // Though it might be tempting to postpone this reobserve call to the // useEffect block, we need getCurrentResult to return an appropriate // loading:true result synchronously (later within the same call to @@ -215,7 +233,7 @@ export function useQueryWithInternalState< getObsQueryOptions( internalState.client, options, - internalState.watchQueryOptions, + watchQueryOptions, internalState.observable ) ); @@ -227,6 +245,7 @@ export function useQueryWithInternalState< internalState.result?.data || internalState.previousData; internalState.result = void 0; } + obsQuery[lastWatchOptions] = watchQueryOptions; } const _callbacks = { @@ -244,22 +263,6 @@ export function useQueryWithInternalState< callbackRef.current = _callbacks; }); - // See if there is an existing observable that was used to fetch the same - // data and if so, use it instead since it will contain the proper queryId - // to fetch the result set. This is used during SSR. - const obsQuery = (internalState.observable = - (renderPromises && - renderPromises.getSSRObservable(internalState.watchQueryOptions)) || - internalState.observable || // Reuse this.observable if possible (and not SSR) - internalState.client.watchQuery( - getObsQueryOptions( - internalState.client, - options, - internalState.watchQueryOptions, - undefined - ) - )); - const obsQueryFields = React.useMemo< Omit, "variables"> >( @@ -283,10 +286,7 @@ export function useQueryWithInternalState< // If SSR has been explicitly disabled, and this function has been called // on the server side, return the default loading state. internalState.result = toQueryResult(ssrDisabledResult, internalState); - } else if ( - options.skip || - internalState.watchQueryOptions.fetchPolicy === "standby" - ) { + } else if (options.skip || watchQueryOptions.fetchPolicy === "standby") { // When skipping a query (ie. we're not querying for data but still want to // render children), make sure the `data` is cleared out and `loading` is // set to `false` (since we aren't loading anything). From 635a32ba1e35b312bee15ee589540c1fccd34d37 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 5 Jun 2024 14:36:38 +0200 Subject: [PATCH 22/62] move observable fully into a readonly prop on internalState --- src/react/hooks/useQuery.ts | 139 ++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 71 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 3178029cbe4..9441b67e4c9 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -61,11 +61,11 @@ export interface InternalState { readonly client: ReturnType; query: DocumentNode | TypedDocumentNode; - observable: ObsQueryWithMeta; + readonly observable: ObsQueryWithMeta; // These members are populated by getCurrentResult and setResult, and it's // okay/normal for them to be initially undefined. - result: undefined | InternalQueryResult; - previousData: undefined | TData; + result?: undefined | InternalQueryResult; + previousData?: undefined | TData; } interface Callbacks { @@ -149,25 +149,57 @@ export function useQueryWithInternalState< options: QueryHookOptions, NoInfer> ) { const client = useApolloClient(options.client); + + // The renderPromises field gets initialized here in the useQuery method, at + // the beginning of everything (for a given component rendering, at least), + // so we can safely use this.renderPromises in other/later InternalState + // methods without worrying it might be uninitialized. Even after + // initialization, this.renderPromises is usually undefined (unless SSR is + // happening), but that's fine as long as it has been initialized that way, + // rather than left uninitialized. + const renderPromises = React.useContext(getApolloContext()).renderPromises; + + const makeWatchQueryOptions = ( + observable?: ObservableQuery + ) => + createWatchQueryOptions( + client, + query, + options, + !!renderPromises, + observable + ); + function createInternalState(previous?: InternalState) { verifyDocumentType(query, DocumentType.Query); - // Reuse previousData from previous InternalState (if any) to provide - // continuity of previousData even if/when the query or client changes. - const previousResult = previous && previous.result; - const previousData = previousResult && previousResult.data; - const internalState: Partial> = { + const internalState: InternalState = { client, query, + observable: + // See if there is an existing observable that was used to fetch the same + // data and if so, use it instead since it will contain the proper queryId + // to fetch the result set. This is used during SSR. + (renderPromises && + renderPromises.getSSRObservable(makeWatchQueryOptions())) || + client.watchQuery( + getObsQueryOptions( + client, + options, + makeWatchQueryOptions(), + undefined + ) + ), + // Reuse previousData from previous InternalState (if any) to provide + // continuity of previousData even if/when the query or client changes. + previousData: previous?.result?.data, }; - if (previousData) { - internalState.previousData = previousData; - } return internalState as InternalState; } - let [internalState, updateState] = React.useState(createInternalState); + let [internalState, updateInternalState] = + React.useState(createInternalState); if (client !== internalState.client || query !== internalState.query) { // If the client or query have changed, we need to create a new InternalState. @@ -176,50 +208,20 @@ export function useQueryWithInternalState< // Since we sometimes trigger some side-effects in the render function, we // re-assign `state` to the new state to ensure that those side-effects are // triggered with the new state. - updateState((internalState = createInternalState(internalState))); + updateInternalState((internalState = createInternalState(internalState))); } - // The renderPromises field gets initialized here in the useQuery method, at - // the beginning of everything (for a given component rendering, at least), - // so we can safely use this.renderPromises in other/later InternalState - // methods without worrying it might be uninitialized. Even after - // initialization, this.renderPromises is usually undefined (unless SSR is - // happening), but that's fine as long as it has been initialized that way, - // rather than left uninitialized. - const renderPromises = React.useContext(getApolloContext()).renderPromises; - + const observable = internalState.observable; const watchQueryOptions: Readonly> = - createWatchQueryOptions( - internalState.client, - internalState.query, - options, - !!renderPromises, - internalState.observable - ); - - // See if there is an existing observable that was used to fetch the same - // data and if so, use it instead since it will contain the proper queryId - // to fetch the result set. This is used during SSR. - const obsQuery: ObsQueryWithMeta = - (internalState.observable = - (renderPromises && renderPromises.getSSRObservable(watchQueryOptions)) || - internalState.observable || // Reuse this.observable if possible (and not SSR) - internalState.client.watchQuery( - getObsQueryOptions( - internalState.client, - options, - watchQueryOptions, - undefined - ) - )); + makeWatchQueryOptions(observable); // TODO: this part is not compatible with any rules of React, and there's // no good way to rewrite it. // it should be moved out into a separate hook that will not be optimized by the compiler { if ( - obsQuery[lastWatchOptions] && - !equal(obsQuery[lastWatchOptions], watchQueryOptions) + observable[lastWatchOptions] && + !equal(observable[lastWatchOptions], watchQueryOptions) ) { // Though it might be tempting to postpone this reobserve call to the // useEffect block, we need getCurrentResult to return an appropriate @@ -229,13 +231,8 @@ export function useQueryWithInternalState< // subscriptions, though it does feel less than ideal that reobserve // (potentially) kicks off a network request (for example, when the // variables have changed), which is technically a side-effect. - internalState.observable.reobserve( - getObsQueryOptions( - internalState.client, - options, - watchQueryOptions, - internalState.observable - ) + observable.reobserve( + getObsQueryOptions(client, options, watchQueryOptions, observable) ); // Make sure getCurrentResult returns a fresh ApolloQueryResult, @@ -245,7 +242,7 @@ export function useQueryWithInternalState< internalState.result?.data || internalState.previousData; internalState.result = void 0; } - obsQuery[lastWatchOptions] = watchQueryOptions; + observable[lastWatchOptions] = watchQueryOptions; } const _callbacks = { @@ -267,19 +264,19 @@ export function useQueryWithInternalState< Omit, "variables"> >( () => ({ - refetch: obsQuery.refetch.bind(obsQuery), - reobserve: obsQuery.reobserve.bind(obsQuery), - fetchMore: obsQuery.fetchMore.bind(obsQuery), - updateQuery: obsQuery.updateQuery.bind(obsQuery), - startPolling: obsQuery.startPolling.bind(obsQuery), - stopPolling: obsQuery.stopPolling.bind(obsQuery), - subscribeToMore: obsQuery.subscribeToMore.bind(obsQuery), + refetch: observable.refetch.bind(observable), + reobserve: observable.reobserve.bind(observable), + fetchMore: observable.fetchMore.bind(observable), + updateQuery: observable.updateQuery.bind(observable), + startPolling: observable.startPolling.bind(observable), + stopPolling: observable.stopPolling.bind(observable), + subscribeToMore: observable.subscribeToMore.bind(observable), }), - [obsQuery] + [observable] ); if ( - (renderPromises || internalState.client.disableNetworkFetches) && + (renderPromises || client.disableNetworkFetches) && options.ssr === false && !options.skip ) { @@ -308,11 +305,11 @@ export function useQueryWithInternalState< const ssrAllowed = !(options.ssr === false || options.skip); if (renderPromises && ssrAllowed) { - renderPromises.registerSSRObservable(obsQuery); + renderPromises.registerSSRObservable(observable); - if (obsQuery.getCurrentResult().loading) { + if (observable.getCurrentResult().loading) { // TODO: This is a legacy API which could probably be cleaned up - renderPromises.addObservableQueryPromise(obsQuery); + renderPromises.addObservableQueryPromise(observable); } } @@ -329,7 +326,7 @@ export function useQueryWithInternalState< // We use `getCurrentResult()` instead of the onNext argument because // the values differ slightly. Specifically, loading results will have // an empty object for data instead of `undefined` for some reason. - const result = obsQuery.getCurrentResult(); + const result = observable.getCurrentResult(); // Make sure we're not attempting to re-render similar results if ( previousResult && @@ -351,7 +348,7 @@ export function useQueryWithInternalState< const onError = (error: Error) => { subscription.unsubscribe(); - subscription = obsQuery.resubscribeAfterError(onNext, onError); + subscription = observable.resubscribeAfterError(onNext, onError); if (!hasOwnProperty.call(error, "graphQLErrors")) { // The error is not a GraphQL error @@ -379,7 +376,7 @@ export function useQueryWithInternalState< } }; - let subscription = obsQuery.subscribe(onNext, onError); + let subscription = observable.subscribe(onNext, onError); // Do the "unsubscribe" with a short delay. // This way, an existing subscription can be reused without an additional @@ -398,7 +395,7 @@ export function useQueryWithInternalState< // useEffect ultimately responsible for the subscription, so we are // effectively passing this dependency array to that useEffect buried // inside useSyncExternalStore, as desired. - obsQuery, + observable, renderPromises, internalState.client.disableNetworkFetches, partialRefetch, From d6de17f7911e5ddf169f96b2d6b6688f42a8c28e Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 5 Jun 2024 17:24:40 +0200 Subject: [PATCH 23/62] remove all direct access to `internalState` after initializing --- src/react/hooks/useLazyQuery.ts | 93 ++++++++++------- src/react/hooks/useQuery.ts | 174 +++++++++++++++++++++----------- 2 files changed, 171 insertions(+), 96 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index d9107569e8f..6f2e74c0663 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -3,6 +3,7 @@ import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import * as React from "rehackt"; import type { + ApolloClient, ApolloQueryResult, OperationVariables, } from "../../core/index.js"; @@ -15,7 +16,11 @@ import type { QueryHookOptions, QueryResult, } from "../types/types.js"; -import type { InternalState } from "./useQuery.js"; +import type { + InternalResult, + ObsQueryWithMeta, + UpdateInternalState, +} from "./useQuery.js"; import { createWatchQueryOptions, getDefaultFetchPolicy, @@ -98,16 +103,19 @@ export function useLazyQuery< skip: !execOptionsRef.current, }; const { - internalState, obsQueryFields, result: useQueryResult, + client, + resultData, + observable, + updateInternalState, } = useQueryWithInternalState(document, queryHookOptions); const initialFetchPolicy = - useQueryResult.observable.options.initialFetchPolicy || + observable.options.initialFetchPolicy || getDefaultFetchPolicy( queryHookOptions.defaultOptions, - internalState.client.defaultOptions + client.defaultOptions ); const forceUpdateState = React.useReducer((tick) => tick + 1, 0)[1]; @@ -160,8 +168,11 @@ export function useLazyQuery< const promise = executeQuery( { ...options, skip: false }, false, - internalState, - forceUpdateState + document, + resultData, + observable, + client, + updateInternalState ).then((queryResult) => Object.assign(queryResult, eagerMethods)); // Because the return value of `useLazyQuery` is usually floated, we need @@ -170,7 +181,15 @@ export function useLazyQuery< return promise; }, - [eagerMethods, forceUpdateState, initialFetchPolicy, internalState] + [ + client, + document, + eagerMethods, + initialFetchPolicy, + observable, + resultData, + updateInternalState, + ] ); const executeRef = React.useRef(execute); @@ -190,38 +209,40 @@ function executeQuery( query?: DocumentNode; }, hasRenderPromises: boolean, - internalState: InternalState, - forceUpdate: () => void + currentQuery: DocumentNode, + resultData: InternalResult, + observable: ObsQueryWithMeta, + client: ApolloClient, + updateInternalState: UpdateInternalState ) { - if (options.query) { - internalState.query = options.query; - } - + const query = options.query || currentQuery; const watchQueryOptions = createWatchQueryOptions( - internalState.client, - internalState.query, + client, + query, options, hasRenderPromises, - internalState.observable + observable ); - const concast = internalState.observable.reobserveAsConcast( - getObsQueryOptions( - internalState.client, - options, - watchQueryOptions, - internalState.observable - ) + const concast = observable.reobserveAsConcast( + getObsQueryOptions(client, options, watchQueryOptions, observable) ); - internalState.observable[lastWatchOptions] = watchQueryOptions; - - // Make sure getCurrentResult returns a fresh ApolloQueryResult, - // but save the current data as this.previousData, just like setResult - // usually does. - internalState.previousData = - internalState.result?.data || internalState.previousData; - internalState.result = void 0; - forceUpdate(); + // this needs to be set to prevent an immediate `resubscribe` in the + // next rerender of the `useQuery` internals + observable[lastWatchOptions] = watchQueryOptions; + updateInternalState({ + client, + observable, + // might be a different query + query, + resultData: Object.assign(resultData, { + // Make sure getCurrentResult returns a fresh ApolloQueryResult, + // but save the current data as this.previousData, just like setResult + // usually does. + previousData: resultData.current?.data || resultData.previousData, + current: undefined, + }), + }); return new Promise< Omit, (typeof EAGER_METHODS)[number]> @@ -239,13 +260,15 @@ function executeQuery( error: () => { resolve( toQueryResult( - internalState.observable.getCurrentResult(), - internalState + observable.getCurrentResult(), + resultData, + observable, + client ) ); }, complete: () => { - resolve(toQueryResult(result, internalState)); + resolve(toQueryResult(result, resultData, observable, client)); }, }); }); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 9441b67e4c9..54055b62799 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -53,21 +53,30 @@ interface InternalQueryResult const noop = () => {}; export const lastWatchOptions = Symbol(); -interface ObsQueryWithMeta +export interface ObsQueryWithMeta extends ObservableQuery { [lastWatchOptions]?: WatchQueryOptions; } -export interface InternalState { - readonly client: ReturnType; - query: DocumentNode | TypedDocumentNode; - readonly observable: ObsQueryWithMeta; +export interface InternalResult { // These members are populated by getCurrentResult and setResult, and it's // okay/normal for them to be initially undefined. - result?: undefined | InternalQueryResult; + current?: undefined | InternalQueryResult; previousData?: undefined | TData; } +interface InternalState { + client: ReturnType; + query: DocumentNode | TypedDocumentNode; + observable: ObsQueryWithMeta; + resultData: InternalResult; +} + +export type UpdateInternalState< + TData, + TVariables extends OperationVariables, +> = (state: InternalState) => void; + interface Callbacks { // Defining these methods as no-ops on the prototype allows us to call // state.onCompleted and/or state.onError without worrying about whether a @@ -190,9 +199,11 @@ export function useQueryWithInternalState< undefined ) ), - // Reuse previousData from previous InternalState (if any) to provide - // continuity of previousData even if/when the query or client changes. - previousData: previous?.result?.data, + resultData: { + // Reuse previousData from previous InternalState (if any) to provide + // continuity of previousData even if/when the query or client changes. + previousData: previous?.resultData.current?.data, + }, }; return internalState as InternalState; @@ -211,7 +222,7 @@ export function useQueryWithInternalState< updateInternalState((internalState = createInternalState(internalState))); } - const observable = internalState.observable; + const { observable, resultData } = internalState; const watchQueryOptions: Readonly> = makeWatchQueryOptions(observable); @@ -238,9 +249,9 @@ export function useQueryWithInternalState< // Make sure getCurrentResult returns a fresh ApolloQueryResult, // but save the current data as this.previousData, just like setResult // usually does. - internalState.previousData = - internalState.result?.data || internalState.previousData; - internalState.result = void 0; + resultData.previousData = + resultData.current?.data || resultData.previousData; + resultData.current = void 0; } observable[lastWatchOptions] = watchQueryOptions; } @@ -282,7 +293,12 @@ export function useQueryWithInternalState< ) { // If SSR has been explicitly disabled, and this function has been called // on the server side, return the default loading state. - internalState.result = toQueryResult(ssrDisabledResult, internalState); + resultData.current = toQueryResult( + ssrDisabledResult, + resultData, + observable, + client + ); } else if (options.skip || watchQueryOptions.fetchPolicy === "standby") { // When skipping a query (ie. we're not querying for data but still want to // render children), make sure the `data` is cleared out and `loading` is @@ -294,12 +310,18 @@ export function useQueryWithInternalState< // previously received data is all of a sudden removed. Unfortunately, // changing this is breaking, so we'll have to wait until Apollo Client 4.0 // to address this. - internalState.result = toQueryResult(skipStandbyResult, internalState); + resultData.current = toQueryResult( + skipStandbyResult, + resultData, + observable, + client + ); } else if ( - internalState.result?.[originalResult] === ssrDisabledResult || - internalState.result?.[originalResult] === skipStandbyResult + resultData.current && + (resultData.current[originalResult] === ssrDisabledResult || + resultData.current[originalResult] === skipStandbyResult) ) { - internalState.result = void 0; + resultData.current = void 0; } const ssrAllowed = !(options.ssr === false || options.skip); @@ -313,16 +335,21 @@ export function useQueryWithInternalState< } } + const disableNetworkFetches = client.disableNetworkFetches; const partialRefetch = options.partialRefetch; const result = useSyncExternalStore( React.useCallback( (handleStoreChange) => { + // reference `disableNetworkFetches` here to ensure that the rules of hooks + // keep it as a dependency of this effect, even though it's not used + disableNetworkFetches; + if (renderPromises) { return () => {}; } const onNext = () => { - const previousResult = internalState.result; + const previousResult = resultData.current; // We use `getCurrentResult()` instead of the onNext argument because // the values differ slightly. Specifically, loading results will have // an empty object for data instead of `undefined` for some reason. @@ -338,11 +365,13 @@ export function useQueryWithInternalState< } setResult( + resultData, result, handleStoreChange, - internalState, callbackRef.current, - partialRefetch + partialRefetch, + observable, + client ); }; @@ -355,13 +384,14 @@ export function useQueryWithInternalState< throw error; } - const previousResult = internalState.result; + const previousResult = resultData.current; if ( !previousResult || (previousResult && previousResult.loading) || !equal(error, previousResult.error) ) { setResult( + resultData, { data: (previousResult && previousResult.data) as TData, error: error as ApolloError, @@ -369,9 +399,10 @@ export function useQueryWithInternalState< networkStatus: NetworkStatus.error, }, handleStoreChange, - internalState, callbackRef.current, - partialRefetch + partialRefetch, + observable, + client ); } }; @@ -386,27 +417,42 @@ export function useQueryWithInternalState< setTimeout(() => subscription.unsubscribe()); }; }, - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/exhaustive-deps + [ - // We memoize the subscribe function using useCallback and the following - // dependency keys, because the subscribe function reference is all that - // useSyncExternalStore uses internally as a dependency key for the - // useEffect ultimately responsible for the subscription, so we are - // effectively passing this dependency array to that useEffect buried - // inside useSyncExternalStore, as desired. - observable, + disableNetworkFetches, renderPromises, - internalState.client.disableNetworkFetches, + observable, + resultData, partialRefetch, + client, ] ), - - () => getCurrentResult(internalState, callbackRef.current, partialRefetch), - () => getCurrentResult(internalState, callbackRef.current, partialRefetch) + () => + getCurrentResult( + resultData, + observable, + callbackRef.current, + partialRefetch, + client + ), + () => + getCurrentResult( + resultData, + observable, + callbackRef.current, + partialRefetch, + client + ) ); - return { result, obsQueryFields, internalState }; + return { + result, + obsQueryFields, + observable, + resultData, + client, + updateInternalState, + }; } // A function to massage options before passing them to ObservableQuery. @@ -503,23 +549,23 @@ export function getObsQueryOptions< } function setResult( + resultData: InternalResult, nextResult: ApolloQueryResult, forceUpdate: () => void, - internalState: InternalState, callbacks: Callbacks, - partialRefetch: boolean | undefined + partialRefetch: boolean | undefined, + observable: ObservableQuery, + client: ApolloClient ) { - const previousResult = internalState.result; + const previousResult = resultData.current; if (previousResult && previousResult.data) { - internalState.previousData = previousResult.data; + resultData.previousData = previousResult.data; } - internalState.result = toQueryResult( - unsafeHandlePartialRefetch( - nextResult, - internalState.observable, - partialRefetch - ), - internalState + resultData.current = toQueryResult( + unsafeHandlePartialRefetch(nextResult, observable, partialRefetch), + resultData, + observable, + client ); // Calling state.setResult always triggers an update, though some call sites // perform additional equality checks before committing to an update. @@ -559,25 +605,29 @@ function handleErrorOrCompleted( } function getCurrentResult( - internalState: InternalState, + resultData: InternalResult, + observable: ObservableQuery, callbacks: Callbacks, - partialRefetch: boolean | undefined + partialRefetch: boolean | undefined, + client: ApolloClient ): InternalQueryResult { // Using this.result as a cache ensures getCurrentResult continues returning // the same (===) result object, unless state.setResult has been called, or // we're doing server rendering and therefore override the result below. - if (!internalState.result) { + if (!resultData.current) { // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION // this could call unsafeHandlePartialRefetch setResult( - internalState.observable.getCurrentResult(), + resultData, + observable.getCurrentResult(), () => {}, - internalState, callbacks, - partialRefetch + partialRefetch, + observable, + client ); } - return internalState.result!; + return resultData.current!; } export function getDefaultFetchPolicy< @@ -604,17 +654,19 @@ function toApolloError( export function toQueryResult( result: ApolloQueryResult, - internalState: InternalState + resultData: InternalResult, + observable: ObservableQuery, + client: ApolloClient ): InternalQueryResult { const { data, partial, ...resultWithoutPartial } = result; const queryResult: InternalQueryResult = { data, // Ensure always defined, even if result.data is missing. ...resultWithoutPartial, - client: internalState.client, - observable: internalState.observable, - variables: internalState.observable.variables, + client: client, + observable: observable, + variables: observable.variables, called: result !== ssrDisabledResult && result !== skipStandbyResult, - previousData: internalState.previousData, + previousData: resultData.previousData, } satisfies Omit< InternalQueryResult, typeof originalResult From 06d98acb3d7cf758220675297d4f02c24a3981c1 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 5 Jun 2024 17:58:41 +0200 Subject: [PATCH 24/62] extract new `useInternalState` hook --- src/react/hooks/useLazyQuery.ts | 4 +- src/react/hooks/useQuery.ts | 79 +++++++++++++++++++++------------ 2 files changed, 53 insertions(+), 30 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 6f2e74c0663..35662ecdd63 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -27,7 +27,7 @@ import { getObsQueryOptions, lastWatchOptions, toQueryResult, - useQueryWithInternalState, + useQueryInternals, } from "./useQuery.js"; // The following methods, when called will execute the query, regardless of @@ -109,7 +109,7 @@ export function useLazyQuery< resultData, observable, updateInternalState, - } = useQueryWithInternalState(document, queryHookOptions); + } = useQueryInternals(document, queryHookOptions); const initialFetchPolicy = observable.options.initialFetchPolicy || diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 54055b62799..6233b414c0e 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -36,6 +36,7 @@ import { maybeDeepFreeze, } from "../../utilities/index.js"; import { wrapHook } from "./internal/index.js"; +import type { RenderPromises } from "../ssr/RenderPromises.js"; const { prototype: { hasOwnProperty }, @@ -143,42 +144,23 @@ function _useQuery< query: DocumentNode | TypedDocumentNode, options: QueryHookOptions, NoInfer> ) { - const { result, obsQueryFields } = useQueryWithInternalState(query, options); + const { result, obsQueryFields } = useQueryInternals(query, options); return React.useMemo( () => ({ ...result, ...obsQueryFields }), [result, obsQueryFields] ); } -export function useQueryWithInternalState< +function useInternalState< TData = any, TVariables extends OperationVariables = OperationVariables, >( - query: DocumentNode | TypedDocumentNode, - options: QueryHookOptions, NoInfer> + client: ApolloClient, + query: DocumentNode | TypedDocumentNode, + options: QueryHookOptions, NoInfer>, + renderPromises: RenderPromises | undefined, + makeWatchQueryOptions: () => WatchQueryOptions ) { - const client = useApolloClient(options.client); - - // The renderPromises field gets initialized here in the useQuery method, at - // the beginning of everything (for a given component rendering, at least), - // so we can safely use this.renderPromises in other/later InternalState - // methods without worrying it might be uninitialized. Even after - // initialization, this.renderPromises is usually undefined (unless SSR is - // happening), but that's fine as long as it has been initialized that way, - // rather than left uninitialized. - const renderPromises = React.useContext(getApolloContext()).renderPromises; - - const makeWatchQueryOptions = ( - observable?: ObservableQuery - ) => - createWatchQueryOptions( - client, - query, - options, - !!renderPromises, - observable - ); - function createInternalState(previous?: InternalState) { verifyDocumentType(query, DocumentType.Query); @@ -219,10 +201,51 @@ export function useQueryWithInternalState< // Since we sometimes trigger some side-effects in the render function, we // re-assign `state` to the new state to ensure that those side-effects are // triggered with the new state. - updateInternalState((internalState = createInternalState(internalState))); + const newInternalState = createInternalState(internalState); + updateInternalState(newInternalState); + return [newInternalState, updateInternalState] as const; } - const { observable, resultData } = internalState; + return [internalState, updateInternalState] as const; +} + +export function useQueryInternals< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: QueryHookOptions, NoInfer> +) { + const client = useApolloClient(options.client); + + // The renderPromises field gets initialized here in the useQuery method, at + // the beginning of everything (for a given component rendering, at least), + // so we can safely use this.renderPromises in other/later InternalState + // methods without worrying it might be uninitialized. Even after + // initialization, this.renderPromises is usually undefined (unless SSR is + // happening), but that's fine as long as it has been initialized that way, + // rather than left uninitialized. + const renderPromises = React.useContext(getApolloContext()).renderPromises; + + const makeWatchQueryOptions = ( + observable?: ObservableQuery + ) => + createWatchQueryOptions( + client, + query, + options, + !!renderPromises, + observable + ); + + const [{ observable, resultData }, updateInternalState] = useInternalState( + client, + query, + options, + renderPromises, + makeWatchQueryOptions + ); + const watchQueryOptions: Readonly> = makeWatchQueryOptions(observable); From 7717b4fe2ab90370ee3ddfd484e5c4a102668ed2 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 5 Jun 2024 18:05:24 +0200 Subject: [PATCH 25/62] extract `useResubscribeIfNecessary` hook --- src/react/hooks/useQuery.ts | 84 ++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 33 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 6233b414c0e..95596a485d2 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -226,6 +226,8 @@ export function useQueryInternals< // happening), but that's fine as long as it has been initialized that way, // rather than left uninitialized. const renderPromises = React.useContext(getApolloContext()).renderPromises; + const disableNetworkFetches = client.disableNetworkFetches; + const ssrAllowed = !(options.ssr === false || options.skip); const makeWatchQueryOptions = ( observable?: ObservableQuery @@ -249,35 +251,13 @@ export function useQueryInternals< const watchQueryOptions: Readonly> = makeWatchQueryOptions(observable); - // TODO: this part is not compatible with any rules of React, and there's - // no good way to rewrite it. - // it should be moved out into a separate hook that will not be optimized by the compiler - { - if ( - observable[lastWatchOptions] && - !equal(observable[lastWatchOptions], watchQueryOptions) - ) { - // Though it might be tempting to postpone this reobserve call to the - // useEffect block, we need getCurrentResult to return an appropriate - // loading:true result synchronously (later within the same call to - // useQuery). Since we already have this.observable here (not true for - // the very first call to useQuery), we are not initiating any new - // subscriptions, though it does feel less than ideal that reobserve - // (potentially) kicks off a network request (for example, when the - // variables have changed), which is technically a side-effect. - observable.reobserve( - getObsQueryOptions(client, options, watchQueryOptions, observable) - ); - - // Make sure getCurrentResult returns a fresh ApolloQueryResult, - // but save the current data as this.previousData, just like setResult - // usually does. - resultData.previousData = - resultData.current?.data || resultData.previousData; - resultData.current = void 0; - } - observable[lastWatchOptions] = watchQueryOptions; - } + useResubscribeIfNecessary( + observable, // might get mutated during render + resultData, // might get mutated during render + watchQueryOptions, + client, + options + ); const _callbacks = { onCompleted: options.onCompleted || noop, @@ -310,7 +290,7 @@ export function useQueryInternals< ); if ( - (renderPromises || client.disableNetworkFetches) && + (renderPromises || disableNetworkFetches) && options.ssr === false && !options.skip ) { @@ -347,8 +327,6 @@ export function useQueryInternals< resultData.current = void 0; } - const ssrAllowed = !(options.ssr === false || options.skip); - if (renderPromises && ssrAllowed) { renderPromises.registerSSRObservable(observable); @@ -358,7 +336,6 @@ export function useQueryInternals< } } - const disableNetworkFetches = client.disableNetworkFetches; const partialRefetch = options.partialRefetch; const result = useSyncExternalStore( React.useCallback( @@ -477,6 +454,47 @@ export function useQueryInternals< updateInternalState, }; } +// this hook is not compatible with any rules of React, and there's no good way to rewrite it. +// it should stay a separate hook that will not be optimized by the compiler +function useResubscribeIfNecessary< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + /** this hook will mutate properties on `observable` */ + observable: ObsQueryWithMeta, + /** this hook will mutate properties on `resultData` */ + resultData: InternalResult, + watchQueryOptions: Readonly>, + client: ApolloClient, + options: QueryHookOptions, NoInfer> +) { + { + if ( + observable[lastWatchOptions] && + !equal(observable[lastWatchOptions], watchQueryOptions) + ) { + // Though it might be tempting to postpone this reobserve call to the + // useEffect block, we need getCurrentResult to return an appropriate + // loading:true result synchronously (later within the same call to + // useQuery). Since we already have this.observable here (not true for + // the very first call to useQuery), we are not initiating any new + // subscriptions, though it does feel less than ideal that reobserve + // (potentially) kicks off a network request (for example, when the + // variables have changed), which is technically a side-effect. + observable.reobserve( + getObsQueryOptions(client, options, watchQueryOptions, observable) + ); + + // Make sure getCurrentResult returns a fresh ApolloQueryResult, + // but save the current data as this.previousData, just like setResult + // usually does. + resultData.previousData = + resultData.current?.data || resultData.previousData; + resultData.current = void 0; + } + observable[lastWatchOptions] = watchQueryOptions; + } +} // A function to massage options before passing them to ObservableQuery. export function createWatchQueryOptions< From 3889fffe1bdb60bf51bd3d46bb742295c0c48ab8 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 5 Jun 2024 18:07:13 +0200 Subject: [PATCH 26/62] add comment --- src/react/hooks/useQuery.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 95596a485d2..b6d19597f57 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -320,6 +320,8 @@ export function useQueryInternals< client ); } else if ( + // reset result if the last render was skipping for some reason, + // but this render isn't skipping anymore resultData.current && (resultData.current[originalResult] === ssrDisabledResult || resultData.current[originalResult] === skipStandbyResult) From da4b8405764926393dfecf99cdca396f499a6205 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 7 Jun 2024 12:05:37 +0200 Subject: [PATCH 27/62] extract `bindObservableMethods` --- src/react/hooks/useQuery.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index b6d19597f57..1b62d02c113 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -276,18 +276,7 @@ export function useQueryInternals< const obsQueryFields = React.useMemo< Omit, "variables"> - >( - () => ({ - refetch: observable.refetch.bind(observable), - reobserve: observable.reobserve.bind(observable), - fetchMore: observable.fetchMore.bind(observable), - updateQuery: observable.updateQuery.bind(observable), - startPolling: observable.startPolling.bind(observable), - stopPolling: observable.stopPolling.bind(observable), - subscribeToMore: observable.subscribeToMore.bind(observable), - }), - [observable] - ); + >(() => bindObservableMethods(observable), [observable]); if ( (renderPromises || disableNetworkFetches) && @@ -769,3 +758,17 @@ const skipStandbyResult = maybeDeepFreeze({ error: void 0, networkStatus: NetworkStatus.ready, }); + +function bindObservableMethods( + observable: ObservableQuery +) { + return { + refetch: observable.refetch.bind(observable), + reobserve: observable.reobserve.bind(observable), + fetchMore: observable.fetchMore.bind(observable), + updateQuery: observable.updateQuery.bind(observable), + startPolling: observable.startPolling.bind(observable), + stopPolling: observable.stopPolling.bind(observable), + subscribeToMore: observable.subscribeToMore.bind(observable), + }; +} From f18b753f374eed2b2be268e550683f3c8d7245cd Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 7 Jun 2024 12:16:30 +0200 Subject: [PATCH 28/62] extract `useHandleSkip` and `useRegisterSSRObservable` hooks --- src/react/hooks/useQuery.ts | 132 +++++++++++++++++++++++------------- 1 file changed, 85 insertions(+), 47 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 1b62d02c113..8d1cef30eb3 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -278,54 +278,21 @@ export function useQueryInternals< Omit, "variables"> >(() => bindObservableMethods(observable), [observable]); - if ( - (renderPromises || disableNetworkFetches) && - options.ssr === false && - !options.skip - ) { - // If SSR has been explicitly disabled, and this function has been called - // on the server side, return the default loading state. - resultData.current = toQueryResult( - ssrDisabledResult, - resultData, - observable, - client - ); - } else if (options.skip || watchQueryOptions.fetchPolicy === "standby") { - // When skipping a query (ie. we're not querying for data but still want to - // render children), make sure the `data` is cleared out and `loading` is - // set to `false` (since we aren't loading anything). - // - // NOTE: We no longer think this is the correct behavior. Skipping should - // not automatically set `data` to `undefined`, but instead leave the - // previous data in place. In other words, skipping should not mandate that - // previously received data is all of a sudden removed. Unfortunately, - // changing this is breaking, so we'll have to wait until Apollo Client 4.0 - // to address this. - resultData.current = toQueryResult( - skipStandbyResult, - resultData, - observable, - client - ); - } else if ( - // reset result if the last render was skipping for some reason, - // but this render isn't skipping anymore - resultData.current && - (resultData.current[originalResult] === ssrDisabledResult || - resultData.current[originalResult] === skipStandbyResult) - ) { - resultData.current = void 0; - } - - if (renderPromises && ssrAllowed) { - renderPromises.registerSSRObservable(observable); + useHandleSkip( + renderPromises, + disableNetworkFetches, + options, + resultData, + observable, + client, + watchQueryOptions + ); - if (observable.getCurrentResult().loading) { - // TODO: This is a legacy API which could probably be cleaned up - renderPromises.addObservableQueryPromise(observable); - } - } + useRegisterSSRObservable( + renderPromises, + ssrAllowed, + observable + ); const partialRefetch = options.partialRefetch; const result = useSyncExternalStore( @@ -445,6 +412,77 @@ export function useQueryInternals< updateInternalState, }; } +function useRegisterSSRObservable< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + renderPromises: RenderPromises | undefined, + ssrAllowed: boolean, + observable: ObsQueryWithMeta +) { + if (renderPromises && ssrAllowed) { + renderPromises.registerSSRObservable(observable); + + if (observable.getCurrentResult().loading) { + // TODO: This is a legacy API which could probably be cleaned up + renderPromises.addObservableQueryPromise(observable); + } + } +} + +function useHandleSkip< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + renderPromises: RenderPromises | undefined, + disableNetworkFetches: boolean, + options: QueryHookOptions, NoInfer>, + resultData: InternalResult, + observable: ObsQueryWithMeta, + client: ApolloClient, + watchQueryOptions: Readonly> +) { + if ( + (renderPromises || disableNetworkFetches) && + options.ssr === false && + !options.skip + ) { + // If SSR has been explicitly disabled, and this function has been called + // on the server side, return the default loading state. + resultData.current = toQueryResult( + ssrDisabledResult, + resultData, + observable, + client + ); + } else if (options.skip || watchQueryOptions.fetchPolicy === "standby") { + // When skipping a query (ie. we're not querying for data but still want to + // render children), make sure the `data` is cleared out and `loading` is + // set to `false` (since we aren't loading anything). + // + // NOTE: We no longer think this is the correct behavior. Skipping should + // not automatically set `data` to `undefined`, but instead leave the + // previous data in place. In other words, skipping should not mandate that + // previously received data is all of a sudden removed. Unfortunately, + // changing this is breaking, so we'll have to wait until Apollo Client 4.0 + // to address this. + resultData.current = toQueryResult( + skipStandbyResult, + resultData, + observable, + client + ); + } else if ( + // reset result if the last render was skipping for some reason, + // but this render isn't skipping anymore + resultData.current && + (resultData.current[originalResult] === ssrDisabledResult || + resultData.current[originalResult] === skipStandbyResult) + ) { + resultData.current = void 0; + } +} + // this hook is not compatible with any rules of React, and there's no good way to rewrite it. // it should stay a separate hook that will not be optimized by the compiler function useResubscribeIfNecessary< From 8480ce61477ebb613368a58187a5d55693e8fc49 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 7 Jun 2024 12:31:56 +0200 Subject: [PATCH 29/62] extract useObservableSubscriptionResult hook --- src/react/hooks/useQuery.ts | 78 ++++++++++++++++++++++++------------- 1 file changed, 52 insertions(+), 26 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 8d1cef30eb3..5de1e8ececb 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -228,6 +228,7 @@ export function useQueryInternals< const renderPromises = React.useContext(getApolloContext()).renderPromises; const disableNetworkFetches = client.disableNetworkFetches; const ssrAllowed = !(options.ssr === false || options.skip); + const partialRefetch = options.partialRefetch; const makeWatchQueryOptions = ( observable?: ObservableQuery @@ -259,21 +260,6 @@ export function useQueryInternals< options ); - const _callbacks = { - onCompleted: options.onCompleted || noop, - onError: options.onError || noop, - }; - const callbackRef = React.useRef>(_callbacks); - React.useEffect(() => { - // Make sure state.onCompleted and state.onError always reflect the latest - // options.onCompleted and options.onError callbacks provided to useQuery, - // since those functions are often recreated every time useQuery is called. - // Like the forceUpdate method, the versions of these methods inherited from - // InternalState.prototype are empty no-ops, but we can override them on the - // base state object (without modifying the prototype). - callbackRef.current = _callbacks; - }); - const obsQueryFields = React.useMemo< Omit, "variables"> >(() => bindObservableMethods(observable), [observable]); @@ -294,8 +280,56 @@ export function useQueryInternals< observable ); - const partialRefetch = options.partialRefetch; - const result = useSyncExternalStore( + const result = useObservableSubscriptionResult( + disableNetworkFetches, + renderPromises, + resultData, + observable, + { + onCompleted: options.onCompleted || noop, + onError: options.onError || noop, + }, + partialRefetch, + client + ); + + return { + result, + obsQueryFields, + observable, + resultData, + client, + updateInternalState, + }; +} + +function useObservableSubscriptionResult< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + disableNetworkFetches: boolean, + renderPromises: RenderPromises | undefined, + resultData: InternalResult, + observable: ObservableQuery, + callbacks: { + onCompleted: (data: TData) => void; + onError: (error: ApolloError) => void; + }, + partialRefetch: boolean | undefined, + client: ApolloClient +) { + const callbackRef = React.useRef>(callbacks); + React.useEffect(() => { + // Make sure state.onCompleted and state.onError always reflect the latest + // options.onCompleted and options.onError callbacks provided to useQuery, + // since those functions are often recreated every time useQuery is called. + // Like the forceUpdate method, the versions of these methods inherited from + // InternalState.prototype are empty no-ops, but we can override them on the + // base state object (without modifying the prototype). + callbackRef.current = callbacks; + }); + + return useSyncExternalStore( React.useCallback( (handleStoreChange) => { // reference `disableNetworkFetches` here to ensure that the rules of hooks @@ -402,16 +436,8 @@ export function useQueryInternals< client ) ); - - return { - result, - obsQueryFields, - observable, - resultData, - client, - updateInternalState, - }; } + function useRegisterSSRObservable< TData = any, TVariables extends OperationVariables = OperationVariables, From 30b17694627ea8409b54983256bf71d06bde4e9f Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 7 Jun 2024 12:50:05 +0200 Subject: [PATCH 30/62] change some method arguments. remove obsolete comment --- src/react/hooks/useQuery.ts | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 5de1e8ececb..112e5a33824 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -218,14 +218,8 @@ export function useQueryInternals< ) { const client = useApolloClient(options.client); - // The renderPromises field gets initialized here in the useQuery method, at - // the beginning of everything (for a given component rendering, at least), - // so we can safely use this.renderPromises in other/later InternalState - // methods without worrying it might be uninitialized. Even after - // initialization, this.renderPromises is usually undefined (unless SSR is - // happening), but that's fine as long as it has been initialized that way, - // rather than left uninitialized. const renderPromises = React.useContext(getApolloContext()).renderPromises; + const isSyncSSR = !!renderPromises; const disableNetworkFetches = client.disableNetworkFetches; const ssrAllowed = !(options.ssr === false || options.skip); const partialRefetch = options.partialRefetch; @@ -265,10 +259,10 @@ export function useQueryInternals< >(() => bindObservableMethods(observable), [observable]); useHandleSkip( - renderPromises, + resultData, // might get mutated during render + isSyncSSR, disableNetworkFetches, options, - resultData, observable, client, watchQueryOptions @@ -282,7 +276,7 @@ export function useQueryInternals< const result = useObservableSubscriptionResult( disableNetworkFetches, - renderPromises, + isSyncSSR, resultData, observable, { @@ -308,7 +302,7 @@ function useObservableSubscriptionResult< TVariables extends OperationVariables = OperationVariables, >( disableNetworkFetches: boolean, - renderPromises: RenderPromises | undefined, + skipSubscribing: boolean, resultData: InternalResult, observable: ObservableQuery, callbacks: { @@ -336,7 +330,7 @@ function useObservableSubscriptionResult< // keep it as a dependency of this effect, even though it's not used disableNetworkFetches; - if (renderPromises) { + if (skipSubscribing) { return () => {}; } @@ -412,7 +406,7 @@ function useObservableSubscriptionResult< [ disableNetworkFetches, - renderPromises, + skipSubscribing, observable, resultData, partialRefetch, @@ -460,16 +454,17 @@ function useHandleSkip< TData = any, TVariables extends OperationVariables = OperationVariables, >( - renderPromises: RenderPromises | undefined, + /** this hook will mutate properties on `resultData` */ + resultData: InternalResult, + isSyncSSR: boolean, disableNetworkFetches: boolean, options: QueryHookOptions, NoInfer>, - resultData: InternalResult, observable: ObsQueryWithMeta, client: ApolloClient, watchQueryOptions: Readonly> ) { if ( - (renderPromises || disableNetworkFetches) && + (isSyncSSR || disableNetworkFetches) && options.ssr === false && !options.skip ) { From c8968854886f5714752d1791bb3a5a7bf9b71c58 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 7 Jun 2024 12:56:16 +0200 Subject: [PATCH 31/62] curry `createMakeWatchQueryOptions` --- src/react/hooks/useLazyQuery.ts | 11 ++-- src/react/hooks/useQuery.ts | 97 +++++++++++++++++---------------- 2 files changed, 53 insertions(+), 55 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 35662ecdd63..c73dc460f05 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -22,7 +22,7 @@ import type { UpdateInternalState, } from "./useQuery.js"; import { - createWatchQueryOptions, + createMakeWatchQueryOptions, getDefaultFetchPolicy, getObsQueryOptions, lastWatchOptions, @@ -167,7 +167,6 @@ export function useLazyQuery< const promise = executeQuery( { ...options, skip: false }, - false, document, resultData, observable, @@ -208,7 +207,6 @@ function executeQuery( options: QueryHookOptions & { query?: DocumentNode; }, - hasRenderPromises: boolean, currentQuery: DocumentNode, resultData: InternalResult, observable: ObsQueryWithMeta, @@ -216,13 +214,12 @@ function executeQuery( updateInternalState: UpdateInternalState ) { const query = options.query || currentQuery; - const watchQueryOptions = createWatchQueryOptions( + const watchQueryOptions = createMakeWatchQueryOptions( client, query, options, - hasRenderPromises, - observable - ); + false + )(observable); const concast = observable.reobserveAsConcast( getObsQueryOptions(client, options, watchQueryOptions, observable) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 112e5a33824..b1bbbb061b4 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -224,16 +224,12 @@ export function useQueryInternals< const ssrAllowed = !(options.ssr === false || options.skip); const partialRefetch = options.partialRefetch; - const makeWatchQueryOptions = ( - observable?: ObservableQuery - ) => - createWatchQueryOptions( - client, - query, - options, - !!renderPromises, - observable - ); + const makeWatchQueryOptions = createMakeWatchQueryOptions( + client, + query, + options, + isSyncSSR + ); const [{ observable, resultData }, updateInternalState] = useInternalState( client, @@ -546,8 +542,12 @@ function useResubscribeIfNecessary< } } -// A function to massage options before passing them to ObservableQuery. -export function createWatchQueryOptions< +/* + * A function to massage options before passing them to ObservableQuery. + * This is two-step curried because we want to reuse the `make` function, + * but the `observable` might differ between calls to `make`. + */ +export function createMakeWatchQueryOptions< TData = any, TVariables extends OperationVariables = OperationVariables, >( @@ -564,46 +564,47 @@ export function createWatchQueryOptions< // query property that we add below. ...otherOptions }: QueryHookOptions = {}, - hasRenderPromises: boolean, - observable: ObservableQuery | undefined -): WatchQueryOptions { - // This Object.assign is safe because otherOptions is a fresh ...rest object - // that did not exist until just now, so modifications are still allowed. - const watchQueryOptions: WatchQueryOptions = Object.assign( - otherOptions, - { query } - ); + hasRenderPromises: boolean +) { + return ( + observable?: ObservableQuery + ): WatchQueryOptions => { + // This Object.assign is safe because otherOptions is a fresh ...rest object + // that did not exist until just now, so modifications are still allowed. + const watchQueryOptions: WatchQueryOptions = + Object.assign(otherOptions, { query }); - if ( - hasRenderPromises && - (watchQueryOptions.fetchPolicy === "network-only" || - watchQueryOptions.fetchPolicy === "cache-and-network") - ) { - // this behavior was added to react-apollo without explanation in this PR - // https://github.com/apollographql/react-apollo/pull/1579 - watchQueryOptions.fetchPolicy = "cache-first"; - } + if ( + hasRenderPromises && + (watchQueryOptions.fetchPolicy === "network-only" || + watchQueryOptions.fetchPolicy === "cache-and-network") + ) { + // this behavior was added to react-apollo without explanation in this PR + // https://github.com/apollographql/react-apollo/pull/1579 + watchQueryOptions.fetchPolicy = "cache-first"; + } - if (!watchQueryOptions.variables) { - watchQueryOptions.variables = {} as TVariables; - } + if (!watchQueryOptions.variables) { + watchQueryOptions.variables = {} as TVariables; + } - if (skip) { - // When skipping, we set watchQueryOptions.fetchPolicy initially to - // "standby", but we also need/want to preserve the initial non-standby - // fetchPolicy that would have been used if not skipping. - watchQueryOptions.initialFetchPolicy = - watchQueryOptions.initialFetchPolicy || - watchQueryOptions.fetchPolicy || - getDefaultFetchPolicy(defaultOptions, client.defaultOptions); - watchQueryOptions.fetchPolicy = "standby"; - } else if (!watchQueryOptions.fetchPolicy) { - watchQueryOptions.fetchPolicy = - observable?.options.initialFetchPolicy || - getDefaultFetchPolicy(defaultOptions, client.defaultOptions); - } + if (skip) { + // When skipping, we set watchQueryOptions.fetchPolicy initially to + // "standby", but we also need/want to preserve the initial non-standby + // fetchPolicy that would have been used if not skipping. + watchQueryOptions.initialFetchPolicy = + watchQueryOptions.initialFetchPolicy || + watchQueryOptions.fetchPolicy || + getDefaultFetchPolicy(defaultOptions, client.defaultOptions); + watchQueryOptions.fetchPolicy = "standby"; + } else if (!watchQueryOptions.fetchPolicy) { + watchQueryOptions.fetchPolicy = + observable?.options.initialFetchPolicy || + getDefaultFetchPolicy(defaultOptions, client.defaultOptions); + } - return watchQueryOptions; + return watchQueryOptions; + }; } export function getObsQueryOptions< From 1485651e446b90c8b162a406d849ffdebc8078a8 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 7 Jun 2024 14:17:48 +0200 Subject: [PATCH 32/62] bring function and hook argyuments into a common order --- src/react/hooks/useLazyQuery.ts | 19 +++-- src/react/hooks/useQuery.ts | 132 ++++++++++++++++++-------------- 2 files changed, 85 insertions(+), 66 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index c73dc460f05..6adb04f5b04 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -166,11 +166,11 @@ export function useLazyQuery< }); const promise = executeQuery( - { ...options, skip: false }, - document, resultData, observable, client, + document, + { ...options, skip: false }, updateInternalState ).then((queryResult) => Object.assign(queryResult, eagerMethods)); @@ -204,13 +204,13 @@ export function useLazyQuery< } function executeQuery( - options: QueryHookOptions & { - query?: DocumentNode; - }, - currentQuery: DocumentNode, resultData: InternalResult, observable: ObsQueryWithMeta, client: ApolloClient, + currentQuery: DocumentNode, + options: QueryHookOptions & { + query?: DocumentNode; + }, updateInternalState: UpdateInternalState ) { const query = options.query || currentQuery; @@ -222,7 +222,7 @@ function executeQuery( )(observable); const concast = observable.reobserveAsConcast( - getObsQueryOptions(client, options, watchQueryOptions, observable) + getObsQueryOptions(observable, client, options, watchQueryOptions) ); // this needs to be set to prevent an immediate `resubscribe` in the // next rerender of the `useQuery` internals @@ -233,9 +233,8 @@ function executeQuery( // might be a different query query, resultData: Object.assign(resultData, { - // Make sure getCurrentResult returns a fresh ApolloQueryResult, - // but save the current data as this.previousData, just like setResult - // usually does. + // We need to modify the previous `resultData` object as we rely on the + // object reference in other places previousData: resultData.current?.data || resultData.previousData, current: undefined, }), diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index b1bbbb061b4..ac8302521bf 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -1,3 +1,22 @@ +/** + * Function parameters in this file try to follow a common order for the sake of + * readability and consistency. The order is as follows: + * + * resultData + * observable + * client + * query + * options + * watchQueryOptions + * makeWatchQueryOptions + * isSSRAllowed + * disableNetworkFetches + * partialRefetch + * renderPromises + * isSyncSSR + * callbacks + */ +/** */ import { invariant } from "../../utilities/globals/index.js"; import * as React from "rehackt"; @@ -175,10 +194,10 @@ function useInternalState< renderPromises.getSSRObservable(makeWatchQueryOptions())) || client.watchQuery( getObsQueryOptions( + undefined, client, options, - makeWatchQueryOptions(), - undefined + makeWatchQueryOptions() ) ), resultData: { @@ -243,11 +262,11 @@ export function useQueryInternals< makeWatchQueryOptions(observable); useResubscribeIfNecessary( - observable, // might get mutated during render resultData, // might get mutated during render - watchQueryOptions, + observable, // might get mutated during render client, - options + options, + watchQueryOptions ); const obsQueryFields = React.useMemo< @@ -256,31 +275,31 @@ export function useQueryInternals< useHandleSkip( resultData, // might get mutated during render - isSyncSSR, - disableNetworkFetches, - options, observable, client, - watchQueryOptions + options, + watchQueryOptions, + disableNetworkFetches, + isSyncSSR ); useRegisterSSRObservable( + observable, renderPromises, - ssrAllowed, - observable + ssrAllowed ); const result = useObservableSubscriptionResult( - disableNetworkFetches, - isSyncSSR, resultData, observable, + client, + disableNetworkFetches, + partialRefetch, + isSyncSSR, { onCompleted: options.onCompleted || noop, onError: options.onError || noop, - }, - partialRefetch, - client + } ); return { @@ -297,16 +316,17 @@ function useObservableSubscriptionResult< TData = any, TVariables extends OperationVariables = OperationVariables, >( - disableNetworkFetches: boolean, - skipSubscribing: boolean, resultData: InternalResult, observable: ObservableQuery, + client: ApolloClient, + disableNetworkFetches: boolean, + partialRefetch: boolean | undefined, + skipSubscribing: boolean, + callbacks: { onCompleted: (data: TData) => void; onError: (error: ApolloError) => void; - }, - partialRefetch: boolean | undefined, - client: ApolloClient + } ) { const callbackRef = React.useRef>(callbacks); React.useEffect(() => { @@ -347,13 +367,13 @@ function useObservableSubscriptionResult< } setResult( - resultData, result, - handleStoreChange, - callbackRef.current, - partialRefetch, + resultData, observable, - client + client, + partialRefetch, + handleStoreChange, + callbackRef.current ); }; @@ -373,18 +393,18 @@ function useObservableSubscriptionResult< !equal(error, previousResult.error) ) { setResult( - resultData, { data: (previousResult && previousResult.data) as TData, error: error as ApolloError, loading: false, networkStatus: NetworkStatus.error, }, - handleStoreChange, - callbackRef.current, - partialRefetch, + resultData, observable, - client + client, + partialRefetch, + handleStoreChange, + callbackRef.current ); } }; @@ -432,9 +452,9 @@ function useRegisterSSRObservable< TData = any, TVariables extends OperationVariables = OperationVariables, >( + observable: ObsQueryWithMeta, renderPromises: RenderPromises | undefined, - ssrAllowed: boolean, - observable: ObsQueryWithMeta + ssrAllowed: boolean ) { if (renderPromises && ssrAllowed) { renderPromises.registerSSRObservable(observable); @@ -452,12 +472,12 @@ function useHandleSkip< >( /** this hook will mutate properties on `resultData` */ resultData: InternalResult, - isSyncSSR: boolean, - disableNetworkFetches: boolean, - options: QueryHookOptions, NoInfer>, observable: ObsQueryWithMeta, client: ApolloClient, - watchQueryOptions: Readonly> + options: QueryHookOptions, NoInfer>, + watchQueryOptions: Readonly>, + disableNetworkFetches: boolean, + isSyncSSR: boolean ) { if ( (isSyncSSR || disableNetworkFetches) && @@ -506,13 +526,13 @@ function useResubscribeIfNecessary< TData = any, TVariables extends OperationVariables = OperationVariables, >( - /** this hook will mutate properties on `observable` */ - observable: ObsQueryWithMeta, /** this hook will mutate properties on `resultData` */ resultData: InternalResult, - watchQueryOptions: Readonly>, + /** this hook will mutate properties on `observable` */ + observable: ObsQueryWithMeta, client: ApolloClient, - options: QueryHookOptions, NoInfer> + options: QueryHookOptions, NoInfer>, + watchQueryOptions: Readonly> ) { { if ( @@ -528,7 +548,7 @@ function useResubscribeIfNecessary< // (potentially) kicks off a network request (for example, when the // variables have changed), which is technically a side-effect. observable.reobserve( - getObsQueryOptions(client, options, watchQueryOptions, observable) + getObsQueryOptions(observable, client, options, watchQueryOptions) ); // Make sure getCurrentResult returns a fresh ApolloQueryResult, @@ -564,7 +584,7 @@ export function createMakeWatchQueryOptions< // query property that we add below. ...otherOptions }: QueryHookOptions = {}, - hasRenderPromises: boolean + isSyncSSR: boolean ) { return ( observable?: ObservableQuery @@ -575,7 +595,7 @@ export function createMakeWatchQueryOptions< Object.assign(otherOptions, { query }); if ( - hasRenderPromises && + isSyncSSR && (watchQueryOptions.fetchPolicy === "network-only" || watchQueryOptions.fetchPolicy === "cache-and-network") ) { @@ -611,10 +631,10 @@ export function getObsQueryOptions< TData, TVariables extends OperationVariables, >( + observable: ObservableQuery | undefined, client: ApolloClient, queryHookOptions: QueryHookOptions, - watchQueryOptions: Partial>, - observable: ObservableQuery | undefined + watchQueryOptions: Partial> ): WatchQueryOptions { const toMerge: Array>> = []; @@ -641,13 +661,13 @@ export function getObsQueryOptions< } function setResult( - resultData: InternalResult, nextResult: ApolloQueryResult, - forceUpdate: () => void, - callbacks: Callbacks, - partialRefetch: boolean | undefined, + resultData: InternalResult, observable: ObservableQuery, - client: ApolloClient + client: ApolloClient, + partialRefetch: boolean | undefined, + forceUpdate: () => void, + callbacks: Callbacks ) { const previousResult = resultData.current; if (previousResult && previousResult.data) { @@ -710,13 +730,13 @@ function getCurrentResult( // WARNING: SIDE-EFFECTS IN THE RENDER FUNCTION // this could call unsafeHandlePartialRefetch setResult( - resultData, observable.getCurrentResult(), - () => {}, - callbacks, - partialRefetch, + resultData, observable, - client + client, + partialRefetch, + () => {}, + callbacks ); } return resultData.current!; From 70f5aaf84ea60863140552619890bdd9fab73dda Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 7 Jun 2024 15:58:34 +0200 Subject: [PATCH 33/62] Move `onQueryExecuted` into `useInternalState` --- src/react/hooks/useLazyQuery.ts | 32 +++++++----------------------- src/react/hooks/useQuery.ts | 35 +++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 6adb04f5b04..35b89ca6950 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -6,6 +6,7 @@ import type { ApolloClient, ApolloQueryResult, OperationVariables, + WatchQueryOptions, } from "../../core/index.js"; import { mergeOptions } from "../../utilities/index.js"; import type { @@ -16,16 +17,11 @@ import type { QueryHookOptions, QueryResult, } from "../types/types.js"; -import type { - InternalResult, - ObsQueryWithMeta, - UpdateInternalState, -} from "./useQuery.js"; +import type { InternalResult, ObsQueryWithMeta } from "./useQuery.js"; import { createMakeWatchQueryOptions, getDefaultFetchPolicy, getObsQueryOptions, - lastWatchOptions, toQueryResult, useQueryInternals, } from "./useQuery.js"; @@ -108,7 +104,7 @@ export function useLazyQuery< client, resultData, observable, - updateInternalState, + onQueryExecuted, } = useQueryInternals(document, queryHookOptions); const initialFetchPolicy = @@ -171,7 +167,7 @@ export function useLazyQuery< client, document, { ...options, skip: false }, - updateInternalState + onQueryExecuted ).then((queryResult) => Object.assign(queryResult, eagerMethods)); // Because the return value of `useLazyQuery` is usually floated, we need @@ -187,7 +183,7 @@ export function useLazyQuery< initialFetchPolicy, observable, resultData, - updateInternalState, + onQueryExecuted, ] ); @@ -211,7 +207,7 @@ function executeQuery( options: QueryHookOptions & { query?: DocumentNode; }, - updateInternalState: UpdateInternalState + onQueryExecuted: (options: WatchQueryOptions) => void ) { const query = options.query || currentQuery; const watchQueryOptions = createMakeWatchQueryOptions( @@ -224,21 +220,7 @@ function executeQuery( const concast = observable.reobserveAsConcast( getObsQueryOptions(observable, client, options, watchQueryOptions) ); - // this needs to be set to prevent an immediate `resubscribe` in the - // next rerender of the `useQuery` internals - observable[lastWatchOptions] = watchQueryOptions; - updateInternalState({ - client, - observable, - // might be a different query - query, - resultData: Object.assign(resultData, { - // We need to modify the previous `resultData` object as we rely on the - // object reference in other places - previousData: resultData.current?.data || resultData.previousData, - current: undefined, - }), - }); + onQueryExecuted(watchQueryOptions); return new Promise< Omit, (typeof EAGER_METHODS)[number]> diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index ac8302521bf..2191ea0fb62 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -222,10 +222,37 @@ function useInternalState< // triggered with the new state. const newInternalState = createInternalState(internalState); updateInternalState(newInternalState); - return [newInternalState, updateInternalState] as const; + return [newInternalState, onQueryExecuted] as const; } - return [internalState, updateInternalState] as const; + return [internalState, onQueryExecuted] as const; + + /** + * Used by `useLazyQuery` when a new query is executed. + * We keep this logic here since it needs to update things in unsafe + * ways and here we at least can keep track of that in a single place. + */ + function onQueryExecuted( + watchQueryOptions: WatchQueryOptions + ) { + // this needs to be set to prevent an immediate `resubscribe` in the + // next rerender of the `useQuery` internals + Object.assign(internalState.observable, { + [lastWatchOptions]: watchQueryOptions, + }); + const resultData = internalState.resultData; + updateInternalState({ + ...internalState, + // might be a different query + query: watchQueryOptions.query, + resultData: Object.assign(resultData, { + // We need to modify the previous `resultData` object as we rely on the + // object reference in other places + previousData: resultData.current?.data || resultData.previousData, + current: undefined, + }), + }); + } } export function useQueryInternals< @@ -250,7 +277,7 @@ export function useQueryInternals< isSyncSSR ); - const [{ observable, resultData }, updateInternalState] = useInternalState( + const [{ observable, resultData }, onQueryExecuted] = useInternalState( client, query, options, @@ -308,7 +335,7 @@ export function useQueryInternals< observable, resultData, client, - updateInternalState, + onQueryExecuted, }; } From fed117bbd63735c573d1889e0bd951a84b8f7e40 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 7 Jun 2024 18:10:18 +0200 Subject: [PATCH 34/62] some style adjustments to be more compiler-friendly --- src/react/hooks/useQuery.ts | 46 ++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 2191ea0fb62..da4321d93f7 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -70,7 +70,7 @@ interface InternalQueryResult [originalResult]: ApolloQueryResult; } -const noop = () => {}; +function noop() {} export const lastWatchOptions = Symbol(); export interface ObsQueryWithMeta @@ -213,20 +213,6 @@ function useInternalState< let [internalState, updateInternalState] = React.useState(createInternalState); - if (client !== internalState.client || query !== internalState.query) { - // If the client or query have changed, we need to create a new InternalState. - // This will trigger a re-render with the new state, but it will also continue - // to run the current render function to completion. - // Since we sometimes trigger some side-effects in the render function, we - // re-assign `state` to the new state to ensure that those side-effects are - // triggered with the new state. - const newInternalState = createInternalState(internalState); - updateInternalState(newInternalState); - return [newInternalState, onQueryExecuted] as const; - } - - return [internalState, onQueryExecuted] as const; - /** * Used by `useLazyQuery` when a new query is executed. * We keep this logic here since it needs to update things in unsafe @@ -253,6 +239,20 @@ function useInternalState< }), }); } + + if (client !== internalState.client || query !== internalState.query) { + // If the client or query have changed, we need to create a new InternalState. + // This will trigger a re-render with the new state, but it will also continue + // to run the current render function to completion. + // Since we sometimes trigger some side-effects in the render function, we + // re-assign `state` to the new state to ensure that those side-effects are + // triggered with the new state. + const newInternalState = createInternalState(internalState); + updateInternalState(newInternalState); + return [newInternalState, onQueryExecuted] as const; + } + + return [internalState, onQueryExecuted] as const; } export function useQueryInternals< @@ -405,8 +405,11 @@ function useObservableSubscriptionResult< }; const onError = (error: Error) => { - subscription.unsubscribe(); - subscription = observable.resubscribeAfterError(onNext, onError); + subscription.current.unsubscribe(); + subscription.current = observable.resubscribeAfterError( + onNext, + onError + ); if (!hasOwnProperty.call(error, "graphQLErrors")) { // The error is not a GraphQL error @@ -436,14 +439,19 @@ function useObservableSubscriptionResult< } }; - let subscription = observable.subscribe(onNext, onError); + // TODO evaluate if we keep this in + // React Compiler cannot handle scoped `let` access, but a mutable object + // like this is fine. + // was: + // let subscription = observable.subscribe(onNext, onError); + const subscription = { current: observable.subscribe(onNext, onError) }; // Do the "unsubscribe" with a short delay. // This way, an existing subscription can be reused without an additional // request if "unsubscribe" and "resubscribe" to the same ObservableQuery // happen in very fast succession. return () => { - setTimeout(() => subscription.unsubscribe()); + setTimeout(() => subscription.current.unsubscribe()); }; }, From a69327cce1b36e8855258e9b19427511e0af8748 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 3 Jul 2024 11:24:17 +0200 Subject: [PATCH 35/62] remove R19 exception from test, chores --- .changeset/nasty-olives-act.md | 5 ++++ .size-limits.json | 4 +-- src/react/hooks/__tests__/useQuery.test.tsx | 29 +-------------------- 3 files changed, 8 insertions(+), 30 deletions(-) create mode 100644 .changeset/nasty-olives-act.md diff --git a/.changeset/nasty-olives-act.md b/.changeset/nasty-olives-act.md new file mode 100644 index 00000000000..02fadec8cae --- /dev/null +++ b/.changeset/nasty-olives-act.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Rewrite big parts of `useQuery` and `useLazyQuery` to be more compliant with the Rules of React and React Compiler diff --git a/.size-limits.json b/.size-limits.json index 09bf55362fa..ee1af372b82 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39604, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32852 + "dist/apollo-client.min.cjs": 39808, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32851 } diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index bfcd534c7e3..7909f8673d8 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -38,7 +38,6 @@ import { useApolloClient } from "../useApolloClient"; import { useLazyQuery } from "../useLazyQuery"; const IS_REACT_17 = React.version.startsWith("17"); -const IS_REACT_19 = React.version.startsWith("19"); describe("useQuery Hook", () => { describe("General use", () => { @@ -1536,33 +1535,7 @@ describe("useQuery Hook", () => { function checkObservableQueries(expectedLinkCount: number) { const obsQueries = client.getObservableQueries("all"); - /* -This is due to a timing change in React 19 - -In React 18, you observe this pattern: - - 1. render - 2. useState initializer - 3. component continues to render with first state - 4. strictMode: render again - 5. strictMode: call useState initializer again - 6. component continues to render with second state - -now, in React 19 it looks like this: - - 1. render - 2. useState initializer - 3. strictMode: call useState initializer again - 4. component continues to render with one of these two states - 5. strictMode: render again - 6. component continues to render with the same state as during the first render - -Since useQuery breaks the rules of React and mutably creates an ObservableQuery on the state during render if none is present, React 18 did create two, while React 19 only creates one. - -This is pure coincidence though, and the useQuery rewrite that doesn't break the rules of hooks as much and creates the ObservableQuery as part of the state initializer will end up with behaviour closer to the old React 18 behaviour again. - -*/ - expect(obsQueries.size).toBe(IS_REACT_19 ? 1 : 2); + expect(obsQueries.size).toBe(2); const activeSet = new Set(); const inactiveSet = new Set(); From 98e44f74cb7c7e93a81bdc7492c9218bf4a2dcd4 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 3 Jul 2024 13:27:34 +0200 Subject: [PATCH 36/62] useSubscription: fix rules of React violations (#11863) * Revert "Merge pull request #9707 from kazekyo/fix_usesubscription_in_strict_mode" This reverts commit 4571e1ad0c2d2b9d9bf072f0f004b4487d55bc76, reversing changes made to 5be85a0ee22a5e4812d1a3da2b036b1d1580b4a5. * essentially rewrite useSubscription * use `setResult` update method * adjust tests * change observable during render; lazy initialization of subscription * no more need for stable options, performance optimization * changeset * adjust documentation * review feedback * Apply suggestions from code review Co-authored-by: Jerel Miller * clarify comment * update size-limits * fix test --------- Co-authored-by: Jerel Miller --- .changeset/little-suits-return.md | 5 + .size-limits.json | 2 +- .../__tests__/client/Subscription.test.tsx | 363 +++++++++--------- .../subscriptions/subscriptions.test.tsx | 20 +- src/react/hooks/useSubscription.ts | 331 +++++++++------- .../__tests__/mockSubscriptionLink.test.tsx | 5 +- 6 files changed, 370 insertions(+), 356 deletions(-) create mode 100644 .changeset/little-suits-return.md diff --git a/.changeset/little-suits-return.md b/.changeset/little-suits-return.md new file mode 100644 index 00000000000..6232f48b7fd --- /dev/null +++ b/.changeset/little-suits-return.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Reimplement `useSubscription` to fix rules of React violations. diff --git a/.size-limits.json b/.size-limits.json index 09bf55362fa..957c176449a 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39604, + "dist/apollo-client.min.cjs": 39619, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32852 } diff --git a/src/react/components/__tests__/client/Subscription.test.tsx b/src/react/components/__tests__/client/Subscription.test.tsx index 4584913a30d..ec4deaa629c 100644 --- a/src/react/components/__tests__/client/Subscription.test.tsx +++ b/src/react/components/__tests__/client/Subscription.test.tsx @@ -5,10 +5,10 @@ import { render, waitFor } from "@testing-library/react"; import { ApolloClient } from "../../../../core"; import { InMemoryCache as Cache } from "../../../../cache"; import { ApolloProvider } from "../../../context"; -import { ApolloLink, Operation } from "../../../../link/core"; +import { ApolloLink, DocumentNode, Operation } from "../../../../link/core"; import { itAsync, MockSubscriptionLink } from "../../../../testing"; import { Subscription } from "../../Subscription"; -import { spyOnConsole } from "../../../../testing/internal"; +import { profile, spyOnConsole } from "../../../../testing/internal"; const results = [ "Luke Skywalker", @@ -422,77 +422,74 @@ describe("should update", () => { cache: new Cache({ addTypename: false }), }); - let count = 0; - let testFailures: any[] = []; + function Container() { + return ( + + {(r: any) => { + ProfiledContainer.replaceSnapshot(r); + return null; + }} + + ); + } + const ProfiledContainer = profile({ + Component: Container, + }); - class Component extends React.Component { - state = { - client: client, - }; + const { rerender } = render( + + + + ); - render() { - return ( - - - {(result: any) => { - const { loading, data } = result; - try { - switch (count++) { - case 0: - expect(loading).toBeTruthy(); - expect(data).toBeUndefined(); - break; - case 1: - setTimeout(() => { - this.setState( - { - client: client2, - }, - () => { - link2.simulateResult(results[1]); - } - ); - }); - // fallthrough - case 2: - expect(loading).toBeFalsy(); - expect(data).toEqual(results[0].result.data); - break; - case 3: - expect(loading).toBeTruthy(); - expect(data).toBeUndefined(); - break; - case 4: - expect(loading).toBeFalsy(); - expect(data).toEqual(results[1].result.data); - break; - default: - throw new Error("too many rerenders"); - } - } catch (error) { - testFailures.push(error); - } - return null; - }} - - - ); - } + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); } - render(); - link.simulateResult(results[0]); - await waitFor(() => { - if (testFailures.length > 0) { - throw testFailures[0]; - } - expect(count).toBe(5); - }); + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeFalsy(); + expect(data).toEqual(results[0].result.data); + } + + await expect(ProfiledContainer).not.toRerender({ timeout: 50 }); + + rerender( + + + + ); + + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); + } + + link2.simulateResult(results[1]); + + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeFalsy(); + expect(data).toEqual(results[1].result.data); + } + + await expect(ProfiledContainer).not.toRerender({ timeout: 50 }); }); - itAsync("if the query changes", (resolve, reject) => { + it("if the query changes", async () => { const subscriptionHero = gql` subscription HeroInfo { hero { @@ -524,72 +521,71 @@ describe("should update", () => { cache: new Cache({ addTypename: false }), }); - let count = 0; - - class Component extends React.Component { - state = { - subscription, - }; - - render() { - return ( - - {(result: any) => { - const { loading, data } = result; - try { - switch (count) { - case 0: - expect(loading).toBeTruthy(); - expect(data).toBeUndefined(); - break; - case 1: - setTimeout(() => { - this.setState( - { - subscription: subscriptionHero, - }, - () => { - heroLink.simulateResult(heroResult); - } - ); - }); - // fallthrough - case 2: - expect(loading).toBeFalsy(); - expect(data).toEqual(results[0].result.data); - break; - case 3: - expect(loading).toBeTruthy(); - expect(data).toBeUndefined(); - break; - case 4: - expect(loading).toBeFalsy(); - expect(data).toEqual(heroResult.result.data); - break; - } - } catch (error) { - reject(error); - } - count++; - return null; - }} - - ); - } + function Container({ subscription }: { subscription: DocumentNode }) { + return ( + + {(r: any) => { + ProfiledContainer.replaceSnapshot(r); + return null; + }} + + ); } + const ProfiledContainer = profile({ + Component: Container, + }); - render( - - - + const { rerender } = render( + , + { + wrapper: ({ children }) => ( + {children} + ), + } ); + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); + } userLink.simulateResult(results[0]); - waitFor(() => expect(count).toBe(5)).then(resolve, reject); + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeFalsy(); + expect(data).toEqual(results[0].result.data); + } + + await expect(ProfiledContainer).not.toRerender({ timeout: 50 }); + + rerender(); + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); + } + + heroLink.simulateResult(heroResult); + + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeFalsy(); + expect(data).toEqual(heroResult.result.data); + } + + await expect(ProfiledContainer).not.toRerender({ timeout: 50 }); }); - itAsync("if the variables change", (resolve, reject) => { + it("if the variables change", async () => { const subscriptionWithVariables = gql` subscription UserInfo($name: String) { user(name: $name) { @@ -620,75 +616,72 @@ describe("should update", () => { cache, }); - let count = 0; + function Container({ variables }: { variables: any }) { + return ( + + {(r: any) => { + ProfiledContainer.replaceSnapshot(r); + return null; + }} + + ); + } + const ProfiledContainer = profile({ + Component: Container, + }); - class Component extends React.Component { - state = { - variables: variablesLuke, - }; + const { rerender } = render( + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - render() { - return ( - - {(result: any) => { - const { loading, data } = result; - try { - switch (count) { - case 0: - expect(loading).toBeTruthy(); - expect(data).toBeUndefined(); - break; - case 1: - setTimeout(() => { - this.setState( - { - variables: variablesHan, - }, - () => { - mockLink.simulateResult({ - result: { data: dataHan }, - }); - } - ); - }); - // fallthrough - case 2: - expect(loading).toBeFalsy(); - expect(data).toEqual(dataLuke); - break; - case 3: - expect(loading).toBeTruthy(); - expect(data).toBeUndefined(); - break; - case 4: - expect(loading).toBeFalsy(); - expect(data).toEqual(dataHan); - break; - } - } catch (error) { - reject(error); - } + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); + } + mockLink.simulateResult({ result: { data: dataLuke } }); - count++; - return null; - }} - - ); - } + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeFalsy(); + expect(data).toEqual(dataLuke); } - render( - - - - ); + await expect(ProfiledContainer).not.toRerender({ timeout: 50 }); - mockLink.simulateResult({ result: { data: dataLuke } }); + rerender(); + + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeTruthy(); + expect(data).toBeUndefined(); + } + mockLink.simulateResult({ + result: { data: dataHan }, + }); + { + const { + snapshot: { loading, data }, + } = await ProfiledContainer.takeRender(); + expect(loading).toBeFalsy(); + expect(data).toEqual(dataHan); + } - waitFor(() => expect(count).toBe(5)).then(resolve, reject); + await expect(ProfiledContainer).not.toRerender({ timeout: 50 }); }); }); diff --git a/src/react/hoc/__tests__/subscriptions/subscriptions.test.tsx b/src/react/hoc/__tests__/subscriptions/subscriptions.test.tsx index b81567ca0d5..ecb517fd14e 100644 --- a/src/react/hoc/__tests__/subscriptions/subscriptions.test.tsx +++ b/src/react/hoc/__tests__/subscriptions/subscriptions.test.tsx @@ -11,8 +11,6 @@ import { itAsync, MockSubscriptionLink } from "../../../../testing"; import { graphql } from "../../graphql"; import { ChildProps } from "../../types"; -const IS_REACT_18 = React.version.startsWith("18"); - describe("subscriptions", () => { let error: typeof console.error; @@ -301,29 +299,17 @@ describe("subscriptions", () => { if (count === 0) expect(user).toEqual(results[0].result.data.user); if (count === 1) { - if (IS_REACT_18) { - expect(user).toEqual(results[1].result.data.user); - } else { - expect(user).toEqual(results[0].result.data.user); - } + expect(user).toEqual(results[0].result.data.user); } if (count === 2) expect(user).toEqual(results[2].result.data.user); if (count === 3) expect(user).toEqual(results[2].result.data.user); if (count === 4) { - if (IS_REACT_18) { - expect(user).toEqual(results[2].result.data.user); - } else { - expect(user).toEqual(results3[2].result.data.user); - } + expect(user).toEqual(results3[2].result.data.user); } if (count === 5) { - if (IS_REACT_18) { - expect(user).toEqual(results3[3].result.data.user); - } else { - expect(user).toEqual(results3[2].result.data.user); - } + expect(user).toEqual(results3[2].result.data.user); resolve(); } } catch (e) { diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts index 0ba57c64346..3b0d7f4303d 100644 --- a/src/react/hooks/useSubscription.ts +++ b/src/react/hooks/useSubscription.ts @@ -10,8 +10,18 @@ import type { SubscriptionHookOptions, SubscriptionResult, } from "../types/types.js"; -import type { OperationVariables } from "../../core/index.js"; +import type { + ApolloClient, + DefaultContext, + FetchPolicy, + FetchResult, + OperationVariables, +} from "../../core/index.js"; +import { Observable } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; +import { useDeepMemo } from "./internal/useDeepMemo.js"; +import { useSyncExternalStore } from "./useSyncExternalStore.js"; + /** * > Refer to the [Subscriptions](https://www.apollographql.com/docs/react/data/subscriptions/) section for a more in-depth overview of `useSubscription`. * @@ -35,11 +45,11 @@ import { useApolloClient } from "./useApolloClient.js"; * } * ``` * @remarks - * #### Subscriptions and React 18 Automatic Batching - * - * With React 18's [automatic batching](https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching), multiple state updates may be grouped into a single re-render for better performance. + * #### Consider using `onData` instead of `useEffect` * - * If your subscription API sends multiple messages at the same time or in very fast succession (within fractions of a millisecond), it is likely that only the last message received in that narrow time frame will result in a re-render. + * If you want to react to incoming data, please use the `onData` option instead of `useEffect`. + * State updates you make inside a `useEffect` hook might cause additional rerenders, and `useEffect` is mostly meant for side effects of rendering, not as an event handler. + * State updates made in an event handler like `onData` might - depending on the React version - be batched and cause only a single rerender. * * Consider the following component: * @@ -61,10 +71,6 @@ import { useApolloClient } from "./useApolloClient.js"; * } * ``` * - * If your subscription back-end emits two messages with the same timestamp, only the last message received by Apollo Client will be rendered. This is because React 18 will batch these two state updates into a single re-render. - * - * Since the component above is using `useEffect` to push `data` into a piece of local state on each `Subscriptions` re-render, the first message will never be added to the `accumulatedData` array since its render was skipped. - * * Instead of using `useEffect` here, we can re-write this component to use the `onData` callback function accepted in `useSubscription`'s `options` object: * * ```jsx @@ -102,24 +108,19 @@ export function useSubscription< TVariables extends OperationVariables = OperationVariables, >( subscription: DocumentNode | TypedDocumentNode, - options?: SubscriptionHookOptions, NoInfer> + options: SubscriptionHookOptions< + NoInfer, + NoInfer + > = Object.create(null) ) { const hasIssuedDeprecationWarningRef = React.useRef(false); - const client = useApolloClient(options?.client); + const client = useApolloClient(options.client); verifyDocumentType(subscription, DocumentType.Subscription); - const [result, setResult] = React.useState< - SubscriptionResult - >({ - loading: !options?.skip, - error: void 0, - data: void 0, - variables: options?.variables, - }); if (!hasIssuedDeprecationWarningRef.current) { hasIssuedDeprecationWarningRef.current = true; - if (options?.onSubscriptionData) { + if (options.onSubscriptionData) { invariant.warn( options.onData ? "'useSubscription' supports only the 'onSubscriptionData' or 'onData' option, but not both. Only the 'onData' option will be used." @@ -127,7 +128,7 @@ export function useSubscription< ); } - if (options?.onSubscriptionComplete) { + if (options.onSubscriptionComplete) { invariant.warn( options.onComplete ? "'useSubscription' supports only the 'onSubscriptionComplete' or 'onComplete' option, but not both. Only the 'onComplete' option will be used." @@ -136,146 +137,178 @@ export function useSubscription< } } - const [observable, setObservable] = React.useState(() => { - if (options?.skip) { - return null; - } + const { skip, fetchPolicy, shouldResubscribe, context } = options; + const variables = useDeepMemo(() => options.variables, [options.variables]); - return client.subscribe({ - query: subscription, - variables: options?.variables, - fetchPolicy: options?.fetchPolicy, - context: options?.context, - }); - }); + let [observable, setObservable] = React.useState(() => + options.skip ? null : ( + createSubscription(client, subscription, variables, fetchPolicy, context) + ) + ); - const canResetObservableRef = React.useRef(false); - React.useEffect(() => { - return () => { - canResetObservableRef.current = true; - }; - }, []); + if (skip) { + if (observable) { + setObservable((observable = null)); + } + } else if ( + !observable || + ((client !== observable.__.client || + subscription !== observable.__.query || + fetchPolicy !== observable.__.fetchPolicy || + !equal(variables, observable.__.variables)) && + (typeof shouldResubscribe === "function" ? + !!shouldResubscribe(options!) + : shouldResubscribe) !== false) + ) { + setObservable( + (observable = createSubscription( + client, + subscription, + variables, + fetchPolicy, + context + )) + ); + } - const ref = React.useRef({ client, subscription, options }); + const optionsRef = React.useRef(options); React.useEffect(() => { - let shouldResubscribe = options?.shouldResubscribe; - if (typeof shouldResubscribe === "function") { - shouldResubscribe = !!shouldResubscribe(options!); - } + optionsRef.current = options; + }); - if (options?.skip) { - if ( - !options?.skip !== !ref.current.options?.skip || - canResetObservableRef.current - ) { - setResult({ - loading: false, - data: void 0, - error: void 0, - variables: options?.variables, - }); - setObservable(null); - canResetObservableRef.current = false; - } - } else if ( - (shouldResubscribe !== false && - (client !== ref.current.client || - subscription !== ref.current.subscription || - options?.fetchPolicy !== ref.current.options?.fetchPolicy || - !options?.skip !== !ref.current.options?.skip || - !equal(options?.variables, ref.current.options?.variables))) || - canResetObservableRef.current - ) { - setResult({ - loading: true, - data: void 0, - error: void 0, - variables: options?.variables, - }); - setObservable( - client.subscribe({ - query: subscription, - variables: options?.variables, - fetchPolicy: options?.fetchPolicy, - context: options?.context, - }) - ); - canResetObservableRef.current = false; - } + const fallbackResult = React.useMemo>( + () => ({ + loading: !skip, + error: void 0, + data: void 0, + variables, + }), + [skip, variables] + ); - Object.assign(ref.current, { client, subscription, options }); - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [client, subscription, options, canResetObservableRef.current]); + return useSyncExternalStore>( + React.useCallback( + (update) => { + if (!observable) { + return () => {}; + } - React.useEffect(() => { - if (!observable) { - return; - } + let subscriptionStopped = false; + const variables = observable.__.variables; + const client = observable.__.client; + const subscription = observable.subscribe({ + next(fetchResult) { + if (subscriptionStopped) { + return; + } - let subscriptionStopped = false; - const subscription = observable.subscribe({ - next(fetchResult) { - if (subscriptionStopped) { - return; - } + const result = { + loading: false, + // TODO: fetchResult.data can be null but SubscriptionResult.data + // expects TData | undefined only + data: fetchResult.data!, + error: void 0, + variables, + }; + observable.__.setResult(result); + update(); - const result = { - loading: false, - // TODO: fetchResult.data can be null but SubscriptionResult.data - // expects TData | undefined only - data: fetchResult.data!, - error: void 0, - variables: options?.variables, - }; - setResult(result); + if (optionsRef.current.onData) { + optionsRef.current.onData({ + client, + data: result, + }); + } else if (optionsRef.current.onSubscriptionData) { + optionsRef.current.onSubscriptionData({ + client, + subscriptionData: result, + }); + } + }, + error(error) { + if (!subscriptionStopped) { + observable.__.setResult({ + loading: false, + data: void 0, + error, + variables, + }); + update(); + optionsRef.current.onError?.(error); + } + }, + complete() { + if (!subscriptionStopped) { + if (optionsRef.current.onComplete) { + optionsRef.current.onComplete(); + } else if (optionsRef.current.onSubscriptionComplete) { + optionsRef.current.onSubscriptionComplete(); + } + } + }, + }); - if (ref.current.options?.onData) { - ref.current.options.onData({ - client, - data: result, - }); - } else if (ref.current.options?.onSubscriptionData) { - ref.current.options.onSubscriptionData({ - client, - subscriptionData: result, - }); - } - }, - error(error) { - if (!subscriptionStopped) { - setResult({ - loading: false, - data: void 0, - error, - variables: options?.variables, + return () => { + // immediately stop receiving subscription values, but do not unsubscribe + // until after a short delay in case another useSubscription hook is + // reusing the same underlying observable and is about to subscribe + subscriptionStopped = true; + setTimeout(() => { + subscription.unsubscribe(); }); - ref.current.options?.onError?.(error); - } - }, - complete() { - if (!subscriptionStopped) { - if (ref.current.options?.onComplete) { - ref.current.options.onComplete(); - } else if (ref.current.options?.onSubscriptionComplete) { - ref.current.options.onSubscriptionComplete(); - } - } + }; }, - }); + [observable] + ), + () => (observable && !skip ? observable.__.result : fallbackResult) + ); +} - return () => { - // immediately stop receiving subscription values, but do not unsubscribe - // until after a short delay in case another useSubscription hook is - // reusing the same underlying observable and is about to subscribe - subscriptionStopped = true; - setTimeout(() => { - subscription.unsubscribe(); - }); - }; - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [observable]); +function createSubscription< + TData = any, + TVariables extends OperationVariables = OperationVariables, +>( + client: ApolloClient, + query: TypedDocumentNode, + variables?: TVariables, + fetchPolicy?: FetchPolicy, + context?: DefaultContext +) { + const __ = { + variables, + client, + query, + fetchPolicy, + result: { + loading: true, + data: void 0, + error: void 0, + variables, + } as SubscriptionResult, + setResult(result: SubscriptionResult) { + __.result = result; + }, + }; - return result; + let observable: Observable> | null = null; + return Object.assign( + new Observable>((observer) => { + // lazily start the subscription when the first observer subscribes + // to get around strict mode + observable ||= client.subscribe({ + query, + variables, + fetchPolicy, + context, + }); + const sub = observable.subscribe(observer); + return () => sub.unsubscribe(); + }), + { + /** + * A tracking object to store details about the observable and the latest result of the subscription. + */ + __, + } + ); } diff --git a/src/testing/react/__tests__/mockSubscriptionLink.test.tsx b/src/testing/react/__tests__/mockSubscriptionLink.test.tsx index 8b26aea0dd3..31c3abe70f7 100644 --- a/src/testing/react/__tests__/mockSubscriptionLink.test.tsx +++ b/src/testing/react/__tests__/mockSubscriptionLink.test.tsx @@ -8,9 +8,6 @@ import { InMemoryCache as Cache } from "../../../cache"; import { ApolloProvider } from "../../../react/context"; import { useSubscription } from "../../../react/hooks"; -const IS_REACT_18 = React.version.startsWith("18"); -const IS_REACT_19 = React.version.startsWith("19"); - describe("mockSubscriptionLink", () => { it("should work with multiple subscribers to the same mock websocket", async () => { const subscription = gql` @@ -65,7 +62,7 @@ describe("mockSubscriptionLink", () => { ); - const numRenders = IS_REACT_18 || IS_REACT_19 ? 2 : results.length + 1; + const numRenders = results.length + 1; // automatic batching in React 18 means we only see 2 renders vs. 5 in v17 await waitFor( From 987aaadca203b1dc62839b234e7cf173dc299d20 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 4 Jul 2024 13:23:58 +0200 Subject: [PATCH 37/62] Apply suggestions from code review Co-authored-by: Jerel Miller --- .size-limits.json | 2 +- src/react/hooks/useLazyQuery.ts | 9 ++-- src/react/hooks/useQuery.ts | 83 ++++++++++++++------------------- 3 files changed, 41 insertions(+), 53 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index ee1af372b82..c3eda5f3708 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39808, + "dist/apollo-client.min.cjs": 39819, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32851 } diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 35b89ca6950..911d2b9e69c 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -25,6 +25,7 @@ import { toQueryResult, useQueryInternals, } from "./useQuery.js"; +import { useIsomorphicLayoutEffect } from "./internal/useIsomorphicLayoutEffect.js"; // The following methods, when called will execute the query, regardless of // whether the useLazyQuery execute function was called before. @@ -188,7 +189,7 @@ export function useLazyQuery< ); const executeRef = React.useRef(execute); - React.useLayoutEffect(() => { + useIsomorphicLayoutEffect(() => { executeRef.current = execute; }); @@ -239,14 +240,16 @@ function executeQuery( resolve( toQueryResult( observable.getCurrentResult(), - resultData, + resultData.previousData, observable, client ) ); }, complete: () => { - resolve(toQueryResult(result, resultData, observable, client)); + resolve( + toQueryResult(result, resultData.previousData, observable, client) + ); }, }); }); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index da4321d93f7..f3ef9aacb0e 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -193,12 +193,7 @@ function useInternalState< (renderPromises && renderPromises.getSSRObservable(makeWatchQueryOptions())) || client.watchQuery( - getObsQueryOptions( - undefined, - client, - options, - makeWatchQueryOptions() - ) + getObsQueryOptions(void 0, client, options, makeWatchQueryOptions()) ), resultData: { // Reuse previousData from previous InternalState (if any) to provide @@ -267,7 +262,7 @@ export function useQueryInternals< const renderPromises = React.useContext(getApolloContext()).renderPromises; const isSyncSSR = !!renderPromises; const disableNetworkFetches = client.disableNetworkFetches; - const ssrAllowed = !(options.ssr === false || options.skip); + const ssrAllowed = options.ssr !== false && !options.skip; const partialRefetch = options.partialRefetch; const makeWatchQueryOptions = createMakeWatchQueryOptions( @@ -310,11 +305,7 @@ export function useQueryInternals< isSyncSSR ); - useRegisterSSRObservable( - observable, - renderPromises, - ssrAllowed - ); + useRegisterSSRObservable(observable, renderPromises, ssrAllowed); const result = useObservableSubscriptionResult( resultData, @@ -349,7 +340,6 @@ function useObservableSubscriptionResult< disableNetworkFetches: boolean, partialRefetch: boolean | undefined, skipSubscribing: boolean, - callbacks: { onCompleted: (data: TData) => void; onError: (error: ApolloError) => void; @@ -483,11 +473,8 @@ function useObservableSubscriptionResult< ); } -function useRegisterSSRObservable< - TData = any, - TVariables extends OperationVariables = OperationVariables, ->( - observable: ObsQueryWithMeta, +function useRegisterSSRObservable( + observable: ObsQueryWithMeta, renderPromises: RenderPromises | undefined, ssrAllowed: boolean ) { @@ -523,7 +510,7 @@ function useHandleSkip< // on the server side, return the default loading state. resultData.current = toQueryResult( ssrDisabledResult, - resultData, + resultData.previousData, observable, client ); @@ -540,7 +527,7 @@ function useHandleSkip< // to address this. resultData.current = toQueryResult( skipStandbyResult, - resultData, + resultData.previousData, observable, client ); @@ -569,32 +556,30 @@ function useResubscribeIfNecessary< options: QueryHookOptions, NoInfer>, watchQueryOptions: Readonly> ) { - { - if ( - observable[lastWatchOptions] && - !equal(observable[lastWatchOptions], watchQueryOptions) - ) { - // Though it might be tempting to postpone this reobserve call to the - // useEffect block, we need getCurrentResult to return an appropriate - // loading:true result synchronously (later within the same call to - // useQuery). Since we already have this.observable here (not true for - // the very first call to useQuery), we are not initiating any new - // subscriptions, though it does feel less than ideal that reobserve - // (potentially) kicks off a network request (for example, when the - // variables have changed), which is technically a side-effect. - observable.reobserve( - getObsQueryOptions(observable, client, options, watchQueryOptions) - ); - - // Make sure getCurrentResult returns a fresh ApolloQueryResult, - // but save the current data as this.previousData, just like setResult - // usually does. - resultData.previousData = - resultData.current?.data || resultData.previousData; - resultData.current = void 0; - } - observable[lastWatchOptions] = watchQueryOptions; + if ( + observable[lastWatchOptions] && + !equal(observable[lastWatchOptions], watchQueryOptions) + ) { + // Though it might be tempting to postpone this reobserve call to the + // useEffect block, we need getCurrentResult to return an appropriate + // loading:true result synchronously (later within the same call to + // useQuery). Since we already have this.observable here (not true for + // the very first call to useQuery), we are not initiating any new + // subscriptions, though it does feel less than ideal that reobserve + // (potentially) kicks off a network request (for example, when the + // variables have changed), which is technically a side-effect. + observable.reobserve( + getObsQueryOptions(observable, client, options, watchQueryOptions) + ); + + // Make sure getCurrentResult returns a fresh ApolloQueryResult, + // but save the current data as this.previousData, just like setResult + // usually does. + resultData.previousData = + resultData.current?.data || resultData.previousData; + resultData.current = void 0; } + observable[lastWatchOptions] = watchQueryOptions; } /* @@ -710,7 +695,7 @@ function setResult( } resultData.current = toQueryResult( unsafeHandlePartialRefetch(nextResult, observable, partialRefetch), - resultData, + resultData.previousData, observable, client ); @@ -724,7 +709,7 @@ function setResult( ); } -function handleErrorOrCompleted( +function handleErrorOrCompleted( result: ApolloQueryResult, previousResult: ApolloQueryResult | undefined, callbacks: Callbacks @@ -801,7 +786,7 @@ function toApolloError( export function toQueryResult( result: ApolloQueryResult, - resultData: InternalResult, + previousData: TData | undefined, observable: ObservableQuery, client: ApolloClient ): InternalQueryResult { @@ -813,7 +798,7 @@ export function toQueryResult( observable: observable, variables: observable.variables, called: result !== ssrDisabledResult && result !== skipStandbyResult, - previousData: resultData.previousData, + previousData, } satisfies Omit< InternalQueryResult, typeof originalResult From d88c7f8909e3cb31532e8b1fc7dd06be12f35591 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 5 Jul 2024 10:27:55 -0600 Subject: [PATCH 38/62] Add `subscribeToMore` function to `useBackgroundQuery`, `useQueryRefHandlers`, and `useLoadableQuery` (#11923) --- .api-reports/api-report-react.api.md | 12 +- .api-reports/api-report-react_hooks.api.md | 12 +- .api-reports/api-report-react_internal.api.md | 9 +- .api-reports/api-report.api.md | 12 +- .changeset/angry-ravens-mate.md | 5 + .changeset/chilly-dots-shake.md | 5 + .changeset/slimy-balloons-cheat.md | 5 + .size-limits.json | 4 +- src/core/ObservableQuery.ts | 2 + .../__tests__/useBackgroundQuery.test.tsx | 137 ++++++++++- .../hooks/__tests__/useLoadableQuery.test.tsx | 221 +++++++++++++++++- .../__tests__/useQueryRefHandlers.test.tsx | 155 +++++++++++- src/react/hooks/useBackgroundQuery.ts | 16 +- src/react/hooks/useLoadableQuery.ts | 23 +- src/react/hooks/useQueryRefHandlers.ts | 14 +- src/react/hooks/useSuspenseQuery.ts | 8 +- 16 files changed, 602 insertions(+), 38 deletions(-) create mode 100644 .changeset/angry-ravens-mate.md create mode 100644 .changeset/chilly-dots-shake.md create mode 100644 .changeset/slimy-balloons-cheat.md diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 74721b26f76..368a7fdc53f 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -2089,6 +2089,7 @@ UseBackgroundQueryResult // @public (undocumented) export type UseBackgroundQueryResult = { + subscribeToMore: SubscribeToMoreFunction; fetchMore: FetchMoreFunction; refetch: RefetchFunction; }; @@ -2148,6 +2149,7 @@ queryRef: QueryRef | null, handlers: { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + subscribeToMore: SubscribeToMoreFunction; reset: ResetFunction; } ]; @@ -2165,6 +2167,7 @@ export function useQueryRefHandlers { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + subscribeToMore: SubscribeToMoreFunction; } // Warning: (ae-forgotten-export) The symbol "ReactiveVar" needs to be exported by the entry point index.d.ts @@ -2240,8 +2243,6 @@ export interface UseSuspenseQueryResult; - // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts - // // (undocumented) subscribeToMore: SubscribeToMoreFunction; } @@ -2305,9 +2306,10 @@ interface WatchQueryOptions // @public (undocumented) export type UseBackgroundQueryResult = { + subscribeToMore: SubscribeToMoreFunction; fetchMore: FetchMoreFunction; refetch: RefetchFunction; }; @@ -1976,6 +1977,7 @@ queryRef: QueryRef | null, handlers: { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + subscribeToMore: SubscribeToMoreFunction; reset: ResetFunction; } ]; @@ -1996,6 +1998,7 @@ export function useQueryRefHandlers { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + subscribeToMore: SubscribeToMoreFunction; } // Warning: (ae-forgotten-export) The symbol "ReactiveVar" needs to be exported by the entry point index.d.ts @@ -2075,8 +2078,6 @@ export interface UseSuspenseQueryResult; - // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts - // // (undocumented) subscribeToMore: SubscribeToMoreFunction; } @@ -2129,9 +2130,10 @@ interface WatchQueryOptions // @public (undocumented) type UseBackgroundQueryResult = { + subscribeToMore: SubscribeToMoreFunction; fetchMore: FetchMoreFunction; refetch: RefetchFunction; }; @@ -1967,6 +1968,7 @@ function useQueryRefHandlers { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + subscribeToMore: SubscribeToMoreFunction; } // Warning: (ae-forgotten-export) The symbol "UseReadQueryResult" needs to be exported by the entry point index.d.ts @@ -2039,8 +2041,6 @@ interface UseSuspenseQueryResult; - // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts - // // (undocumented) subscribeToMore: SubscribeToMoreFunction; } @@ -2134,8 +2134,9 @@ export function wrapQueryRef(inter // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:29:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/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 // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 95cc0a4694b..4acdff1d7da 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2752,6 +2752,7 @@ UseBackgroundQueryResult // @public (undocumented) export type UseBackgroundQueryResult = { + subscribeToMore: SubscribeToMoreFunction; fetchMore: FetchMoreFunction; refetch: RefetchFunction; }; @@ -2811,6 +2812,7 @@ queryRef: QueryRef | null, handlers: { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + subscribeToMore: SubscribeToMoreFunction; reset: ResetFunction; } ]; @@ -2828,6 +2830,7 @@ export function useQueryRefHandlers { fetchMore: FetchMoreFunction; refetch: RefetchFunction; + subscribeToMore: SubscribeToMoreFunction; } // @public @@ -2901,8 +2904,6 @@ export interface UseSuspenseQueryResult; - // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts - // // (undocumented) subscribeToMore: SubscribeToMoreFunction; } @@ -2994,9 +2995,10 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:29:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useLoadableQuery.ts:107:1 - (ae-forgotten-export) The symbol "ResetFunction" 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/useLoadableQuery.ts:120:9 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.changeset/angry-ravens-mate.md b/.changeset/angry-ravens-mate.md new file mode 100644 index 00000000000..3072009aff1 --- /dev/null +++ b/.changeset/angry-ravens-mate.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Add support for `subscribeToMore` function to `useQueryRefHandlers`. diff --git a/.changeset/chilly-dots-shake.md b/.changeset/chilly-dots-shake.md new file mode 100644 index 00000000000..0bbb1de7e58 --- /dev/null +++ b/.changeset/chilly-dots-shake.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Add support for `subscribeToMore` function to `useLoadableQuery`. diff --git a/.changeset/slimy-balloons-cheat.md b/.changeset/slimy-balloons-cheat.md new file mode 100644 index 00000000000..72291902106 --- /dev/null +++ b/.changeset/slimy-balloons-cheat.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Add support for `subscribeToMore` function to `useBackgroundQuery`. diff --git a/.size-limits.json b/.size-limits.json index 79e71c06997..81ed0bcf995 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39825, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32851 + "dist/apollo-client.min.cjs": 39873, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32865 } diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 47deef22483..f9e6dd6b1e4 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -163,6 +163,8 @@ export class ObservableQuery< this.waitForOwnResult = skipCacheDataFor(options.fetchPolicy); this.isTornDown = false; + this.subscribeToMore = this.subscribeToMore.bind(this); + const { watchQuery: { fetchPolicy: defaultFetchPolicy = "cache-first" } = {}, } = queryManager.defaultOptions; diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 91d142a4df5..ac0ef98b87a 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -16,6 +16,7 @@ import { TypedDocumentNode, ApolloLink, Observable, + split, } from "../../../core"; import { MockedResponse, @@ -29,6 +30,7 @@ import { concatPagination, offsetLimitPagination, DeepPartial, + getMainDefinition, } from "../../../utilities"; import { useBackgroundQuery } from "../useBackgroundQuery"; import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; @@ -37,7 +39,10 @@ import { QueryRef, QueryReference } from "../../internal"; import { InMemoryCache } from "../../../cache"; import { SuspenseQueryHookFetchPolicy } from "../../types/types"; import equal from "@wry/equality"; -import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; +import { + RefetchWritePolicy, + SubscribeToMoreOptions, +} from "../../../core/watchQueryOptions"; import { skipToken } from "../constants"; import { PaginatedCaseData, @@ -54,6 +59,7 @@ import { spyOnConsole, useTrackRenders, } from "../../../testing/internal"; +import { SubscribeToMoreFunction } from "../useSuspenseQuery"; afterEach(() => { jest.useRealTimers(); @@ -6052,6 +6058,135 @@ describe("fetchMore", () => { await expect(Profiler).not.toRerender(); }); + + it("can subscribe to subscriptions and react to cache updates via `subscribeToMore`", async () => { + interface SubscriptionData { + greetingUpdated: string; + } + + type UpdateQueryFn = NonNullable< + SubscribeToMoreOptions< + SimpleCaseData, + Record, + SubscriptionData + >["updateQuery"] + >; + + const subscription: TypedDocumentNode< + SubscriptionData, + Record + > = gql` + subscription { + greetingUpdated + } + `; + + const { mocks, query } = setupSimpleCase(); + + const wsLink = new MockSubscriptionLink(); + const mockLink = new MockLink(mocks); + + const link = split( + ({ query }) => { + const definition = getMainDefinition(query); + + return ( + definition.kind === "OperationDefinition" && + definition.operation === "subscription" + ); + }, + wsLink, + mockLink + ); + + const client = new ApolloClient({ link, cache: new InMemoryCache() }); + + const Profiler = createProfiler({ + initialSnapshot: { + subscribeToMore: null as SubscribeToMoreFunction< + SimpleCaseData, + Record + > | null, + result: null as UseReadQueryResult | null, + }, + }); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef, { subscribeToMore }] = useBackgroundQuery(query); + + Profiler.mergeSnapshot({ subscribeToMore }); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + const updateQuery = jest.fn< + ReturnType, + Parameters + >((_, { subscriptionData: { data } }) => { + return { greeting: data.greetingUpdated }; + }); + + const { snapshot } = Profiler.getCurrentRender(); + + snapshot.subscribeToMore!({ document: subscription, updateQuery }); + + wsLink.simulateResult({ + result: { + data: { + greetingUpdated: "Subscription hello", + }, + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Subscription hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(updateQuery).toHaveBeenCalledTimes(1); + expect(updateQuery).toHaveBeenCalledWith( + { greeting: "Hello" }, + { + subscriptionData: { + data: { greetingUpdated: "Subscription hello" }, + }, + variables: {}, + } + ); + }); }); describe.skip("type tests", () => { diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index a5e97ca52e8..9c83a0bd6c5 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -22,6 +22,8 @@ import { Observable, OperationVariables, RefetchWritePolicy, + SubscribeToMoreOptions, + split, } from "../../../core"; import { MockedProvider, @@ -35,6 +37,7 @@ import { concatPagination, offsetLimitPagination, DeepPartial, + getMainDefinition, } from "../../../utilities"; import { useLoadableQuery } from "../useLoadableQuery"; import type { UseReadQueryResult } from "../useReadQuery"; @@ -43,7 +46,11 @@ import { ApolloProvider } from "../../context"; import { InMemoryCache } from "../../../cache"; import { LoadableQueryHookFetchPolicy } from "../../types/types"; import { QueryRef } from "../../../react"; -import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; +import { + FetchMoreFunction, + RefetchFunction, + SubscribeToMoreFunction, +} from "../useSuspenseQuery"; import invariant, { InvariantError } from "ts-invariant"; import { Profiler, @@ -4667,6 +4674,218 @@ it("allows loadQuery to be called in useEffect on first render", async () => { expect(() => renderWithMocks(, { mocks })).not.toThrow(); }); +it("can subscribe to subscriptions and react to cache updates via `subscribeToMore`", async () => { + interface SubscriptionData { + greetingUpdated: string; + } + + type UpdateQueryFn = NonNullable< + SubscribeToMoreOptions< + SimpleCaseData, + Record, + SubscriptionData + >["updateQuery"] + >; + + const subscription: TypedDocumentNode< + SubscriptionData, + Record + > = gql` + subscription { + greetingUpdated + } + `; + + const { mocks, query } = setupSimpleCase(); + + const wsLink = new MockSubscriptionLink(); + const mockLink = new MockLink(mocks); + + const link = split( + ({ query }) => { + const definition = getMainDefinition(query); + + return ( + definition.kind === "OperationDefinition" && + definition.operation === "subscription" + ); + }, + wsLink, + mockLink + ); + + const client = new ApolloClient({ link, cache: new InMemoryCache() }); + + const Profiler = createProfiler({ + initialSnapshot: { + subscribeToMore: null as SubscribeToMoreFunction< + SimpleCaseData, + Record + > | null, + result: null as UseReadQueryResult | null, + }, + }); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { subscribeToMore }] = useLoadableQuery(query); + + Profiler.mergeSnapshot({ subscribeToMore }); + + return ( +
+ + }> + {queryRef && } + +
+ ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + const updateQuery = jest.fn< + ReturnType, + Parameters + >((_, { subscriptionData: { data } }) => { + return { greeting: data.greetingUpdated }; + }); + + const { snapshot } = Profiler.getCurrentRender(); + + snapshot.subscribeToMore!({ document: subscription, updateQuery }); + + wsLink.simulateResult({ + result: { + data: { + greetingUpdated: "Subscription hello", + }, + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Subscription hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(updateQuery).toHaveBeenCalledTimes(1); + expect(updateQuery).toHaveBeenCalledWith( + { greeting: "Hello" }, + { + subscriptionData: { + data: { greetingUpdated: "Subscription hello" }, + }, + variables: {}, + } + ); +}); + +it("throws when calling `subscribeToMore` before loading the query", async () => { + interface SubscriptionData { + greetingUpdated: string; + } + + const subscription: TypedDocumentNode< + SubscriptionData, + Record + > = gql` + subscription { + greetingUpdated + } + `; + + const { mocks, query } = setupSimpleCase(); + + const wsLink = new MockSubscriptionLink(); + const mockLink = new MockLink(mocks); + + const link = split( + ({ query }) => { + const definition = getMainDefinition(query); + + return ( + definition.kind === "OperationDefinition" && + definition.operation === "subscription" + ); + }, + wsLink, + mockLink + ); + + const client = new ApolloClient({ link, cache: new InMemoryCache() }); + + const Profiler = createProfiler({ + initialSnapshot: { + subscribeToMore: null as SubscribeToMoreFunction< + SimpleCaseData, + Record + > | null, + result: null as UseReadQueryResult | null, + }, + }); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { subscribeToMore }] = useLoadableQuery(query); + + Profiler.mergeSnapshot({ subscribeToMore }); + + return ( +
+ + }> + {queryRef && } + +
+ ); + } + + renderWithClient(, { client, wrapper: Profiler }); + // initial render + await Profiler.takeRender(); + + const { snapshot } = Profiler.getCurrentRender(); + + expect(() => { + snapshot.subscribeToMore!({ document: subscription }); + }).toThrow( + new InvariantError("The query has not been loaded. Please load the query.") + ); +}); + describe.skip("type tests", () => { it("returns unknown when TData cannot be inferred", () => { const query = gql``; diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index 012f7fb1872..536d8ca2edb 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -4,10 +4,16 @@ import { ApolloClient, InMemoryCache, NetworkStatus, + SubscribeToMoreOptions, TypedDocumentNode, gql, + split, } from "../../../core"; -import { MockLink, MockedResponse } from "../../../testing"; +import { + MockLink, + MockSubscriptionLink, + MockedResponse, +} from "../../../testing"; import { PaginatedCaseData, SimpleCaseData, @@ -19,13 +25,14 @@ 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"; import { QueryRef } from "../../internal"; import { useBackgroundQuery } from "../useBackgroundQuery"; import { useLoadableQuery } from "../useLoadableQuery"; -import { concatPagination } from "../../../utilities"; +import { concatPagination, getMainDefinition } from "../../../utilities"; test("does not interfere with updates from useReadQuery", async () => { const { query, mocks } = setupSimpleCase(); @@ -1927,3 +1934,147 @@ test("`fetchMore` works with startTransition from useBackgroundQuery and useQuer await expect(Profiler).not.toRerender(); }); + +test("can subscribe to subscriptions and react to cache updates via `subscribeToMore`", async () => { + interface SubscriptionData { + greetingUpdated: string; + } + + type UpdateQueryFn = NonNullable< + SubscribeToMoreOptions< + SimpleCaseData, + Record, + SubscriptionData + >["updateQuery"] + >; + + const subscription: TypedDocumentNode< + SubscriptionData, + Record + > = gql` + subscription { + greetingUpdated + } + `; + + const { mocks, query } = setupSimpleCase(); + + const wsLink = new MockSubscriptionLink(); + const mockLink = new MockLink(mocks); + + const link = split( + ({ query }) => { + const definition = getMainDefinition(query); + + return ( + definition.kind === "OperationDefinition" && + definition.operation === "subscription" + ); + }, + wsLink, + mockLink + ); + + const client = new ApolloClient({ link, cache: new InMemoryCache() }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + const Profiler = createProfiler({ + initialSnapshot: { + subscribeToMore: null as SubscribeToMoreFunction< + SimpleCaseData, + Record + > | null, + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + // We can ignore the return result here since we are testing the mechanics + // of this hook to ensure it doesn't interfere with the updates from + // useReadQuery + const { subscribeToMore } = useQueryRefHandlers(queryRef); + + Profiler.mergeSnapshot({ subscribeToMore }); + + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + const updateQuery = jest.fn< + ReturnType, + Parameters + >((_, { subscriptionData: { data } }) => { + return { greeting: data.greetingUpdated }; + }); + + const { snapshot } = Profiler.getCurrentRender(); + + snapshot.subscribeToMore!({ document: subscription, updateQuery }); + + wsLink.simulateResult({ + result: { + data: { + greetingUpdated: "Subscription hello", + }, + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Subscription hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(updateQuery).toHaveBeenCalledTimes(1); + expect(updateQuery).toHaveBeenCalledWith( + { greeting: "Hello" }, + { + subscriptionData: { + data: { greetingUpdated: "Subscription hello" }, + }, + variables: {}, + } + ); +}); diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 4b5a5668389..ba8dc1e71fd 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -17,7 +17,11 @@ 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 } from "./useSuspenseQuery.js"; +import type { + FetchMoreFunction, + RefetchFunction, + SubscribeToMoreFunction, +} from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; import type { DeepPartial } from "../../utilities/index.js"; import type { SkipToken } from "./constants.js"; @@ -26,7 +30,11 @@ export type UseBackgroundQueryResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, > = { + /** {@inheritDoc @apollo/client!ObservableQuery#subscribeToMore:member(1)} */ + subscribeToMore: SubscribeToMoreFunction; + /** {@inheritDoc @apollo/client!ObservableQuery#fetchMore:member(1)} */ fetchMore: FetchMoreFunction; + /** {@inheritDoc @apollo/client!ObservableQuery#refetch:member(1)} */ refetch: RefetchFunction; }; @@ -281,6 +289,10 @@ function _useBackgroundQuery< return [ didFetchResult.current ? wrappedQueryRef : void 0, - { fetchMore, refetch }, + { + fetchMore, + refetch, + subscribeToMore: queryRef.observable.subscribeToMore, + }, ]; } diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 15d1e2a7e56..b9aa70d11e2 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -18,7 +18,11 @@ 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 } from "./useSuspenseQuery.js"; +import type { + FetchMoreFunction, + RefetchFunction, + SubscribeToMoreFunction, +} from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; import type { DeepPartial, @@ -49,6 +53,8 @@ export type UseLoadableQueryResult< fetchMore: FetchMoreFunction; /** {@inheritDoc @apollo/client!QueryResultDocumentation#refetch:member} */ refetch: RefetchFunction; + /** {@inheritDoc @apollo/client!ObservableQuery#subscribeToMore:member(1)} */ + subscribeToMore: SubscribeToMoreFunction; /** * A function that resets the `queryRef` back to `null`. */ @@ -255,9 +261,22 @@ export function useLoadableQuery< ] ); + const subscribeToMore: SubscribeToMoreFunction = + React.useCallback( + (options) => { + invariant( + internalQueryRef, + "The query has not been loaded. Please load the query." + ); + + return internalQueryRef.observable.subscribeToMore(options); + }, + [internalQueryRef] + ); + const reset: ResetFunction = React.useCallback(() => { setQueryRef(null); }, []); - return [loadQuery, queryRef, { fetchMore, refetch, reset }]; + return [loadQuery, queryRef, { fetchMore, refetch, reset, subscribeToMore }]; } diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts index 95036eafcf3..a621d579691 100644 --- a/src/react/hooks/useQueryRefHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -8,7 +8,11 @@ import { } from "../internal/index.js"; import type { QueryRef } from "../internal/index.js"; import type { OperationVariables } from "../../core/types.js"; -import type { RefetchFunction, FetchMoreFunction } from "./useSuspenseQuery.js"; +import type { + RefetchFunction, + FetchMoreFunction, + SubscribeToMoreFunction, +} from "./useSuspenseQuery.js"; import type { FetchMoreQueryOptions } from "../../core/watchQueryOptions.js"; import { useApolloClient } from "./useApolloClient.js"; import { wrapHook } from "./internal/index.js"; @@ -21,6 +25,8 @@ export interface UseQueryRefHandlersResult< refetch: RefetchFunction; /** {@inheritDoc @apollo/client!ObservableQuery#fetchMore:member(1)} */ fetchMore: FetchMoreFunction; + /** {@inheritDoc @apollo/client!ObservableQuery#subscribeToMore:member(1)} */ + subscribeToMore: SubscribeToMoreFunction; } /** @@ -112,5 +118,9 @@ function _useQueryRefHandlers< [internalQueryRef] ); - return { refetch, fetchMore }; + return { + refetch, + fetchMore, + subscribeToMore: internalQueryRef.observable.subscribeToMore, + }; } diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index fe438ab6240..e3395390a6b 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -274,13 +274,7 @@ function _useSuspenseQuery< [queryRef] ); - const subscribeToMore: SubscribeToMoreFunction< - TData | undefined, - TVariables - > = React.useCallback( - (options) => queryRef.observable.subscribeToMore(options), - [queryRef] - ); + const subscribeToMore = queryRef.observable.subscribeToMore; return React.useMemo< UseSuspenseQueryResult From 96422ce95b923b560321a88acd2eec35cf2a1c18 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 8 Jul 2024 10:28:18 +0200 Subject: [PATCH 39/62] Add `cause` field to `ApolloError`. (#11902) * Add `cause` field to `ApolloError`. * review feedback: include `protocolErrors` * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: phryneas --- .api-reports/api-report-core.api.md | 4 ++++ .api-reports/api-report-errors.api.md | 4 ++++ .api-reports/api-report-react.api.md | 4 ++++ .../api-report-react_components.api.md | 4 ++++ .api-reports/api-report-react_context.api.md | 4 ++++ .api-reports/api-report-react_hoc.api.md | 4 ++++ .api-reports/api-report-react_hooks.api.md | 4 ++++ .api-reports/api-report-react_internal.api.md | 4 ++++ .api-reports/api-report-react_ssr.api.md | 4 ++++ .api-reports/api-report-testing.api.md | 4 ++++ .api-reports/api-report-testing_core.api.md | 4 ++++ .api-reports/api-report-utilities.api.md | 4 ++++ .api-reports/api-report.api.md | 4 ++++ .changeset/flat-onions-guess.md | 5 +++++ .size-limits.json | 4 ++-- src/errors/index.ts | 18 ++++++++++++++++++ 16 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 .changeset/flat-onions-guess.md diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index de8b71f9153..35789a58440 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -172,6 +172,10 @@ export interface ApolloClientOptions { export class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-errors.api.md b/.api-reports/api-report-errors.api.md index 205b170bf0f..2b3d65a0558 100644 --- a/.api-reports/api-report-errors.api.md +++ b/.api-reports/api-report-errors.api.md @@ -11,6 +11,10 @@ import type { GraphQLErrorExtensions } from 'graphql'; // @public (undocumented) export class ApolloError extends Error { constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 368a7fdc53f..c81d938a887 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -223,6 +223,10 @@ export interface ApolloContextValue { class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index b9bd5eda67e..efb4ddab230 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -201,6 +201,10 @@ interface ApolloClientOptions { class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index bc1878779ad..41aed0f90c4 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -221,6 +221,10 @@ export interface ApolloContextValue { class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index a5cf57a3c8d..18f094dde4c 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -200,6 +200,10 @@ interface ApolloClientOptions { class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index a88a99c06ca..b5ee3e1c051 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -199,6 +199,10 @@ interface ApolloClientOptions { class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index be65e4e3a3f..95a7366593c 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -199,6 +199,10 @@ interface ApolloClientOptions { class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index f4ca55a6daf..ea788b305b9 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -200,6 +200,10 @@ interface ApolloClientOptions { class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index c147f00f820..01ba05ec8b1 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -200,6 +200,10 @@ interface ApolloClientOptions { class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index 1cbfc54605f..545e30231cd 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -199,6 +199,10 @@ interface ApolloClientOptions { class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index c53a249f699..84e1668f11b 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -212,6 +212,10 @@ interface ApolloClientOptions { class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 4acdff1d7da..fb2bec58947 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -195,6 +195,10 @@ export interface ApolloContextValue { export class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + cause: ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) diff --git a/.changeset/flat-onions-guess.md b/.changeset/flat-onions-guess.md new file mode 100644 index 00000000000..ce7dc67887a --- /dev/null +++ b/.changeset/flat-onions-guess.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Add `cause` field to `ApolloError`. diff --git a/.size-limits.json b/.size-limits.json index 81ed0bcf995..5cca69e7256 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39873, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32865 + "dist/apollo-client.min.cjs": 39906, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32896 } diff --git a/src/errors/index.ts b/src/errors/index.ts index 69277055500..3c07411161b 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -81,6 +81,17 @@ export class ApolloError extends Error { }>; public clientErrors: ReadonlyArray; public networkError: Error | ServerParseError | ServerError | null; + /** + * Indicates the specific original cause of the error. + * + * This field contains the first available `networkError`, `graphQLError`, `protocolError`, `clientError`, or `null` if none are available. + */ + public cause: + | ({ + message: string; + extensions?: GraphQLErrorExtensions[]; + } & Partial) + | null; // An object that can be used to provide some additional information // about an error, e.g. specifying the type of error this is. Used @@ -106,6 +117,13 @@ export class ApolloError extends Error { this.networkError = networkError || null; this.message = errorMessage || generateErrorMessage(this); this.extraInfo = extraInfo; + this.cause = + [ + networkError, + ...(graphQLErrors || []), + ...(protocolErrors || []), + ...(clientErrors || []), + ].find((e) => !!e) || null; // We're not using `Object.setPrototypeOf` here as it isn't fully // supported on Android (see issue #3236). From 228429a1d36eae691473b24fb641ec3cd84c8a3d Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 8 Jul 2024 11:26:14 +0200 Subject: [PATCH 40/62] Call `nextFetchPolicy` with "variables-changed" even if there is a `fetchPolicy` option specified. (#11626) * Call `nextFetchPolicy` with "variables-changed" even if there is a `fetchPolicy` specified. fixes #11365 * update size-limits * remove `.only` * Clean up Prettier, Size-limit, and Api-Extractor * use `mockFetchQuery` helper in test * fix detail in test-tsconfig.json --------- Co-authored-by: phryneas --- .changeset/tasty-chairs-dress.md | 5 + .size-limits.json | 4 +- src/core/ObservableQuery.ts | 5 +- src/react/hooks/__tests__/useQuery.test.tsx | 127 ++++++++++++++++++++ src/tsconfig.json | 2 + 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 .changeset/tasty-chairs-dress.md diff --git a/.changeset/tasty-chairs-dress.md b/.changeset/tasty-chairs-dress.md new file mode 100644 index 00000000000..459c72bd44b --- /dev/null +++ b/.changeset/tasty-chairs-dress.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Call `nextFetchPolicy` with "variables-changed" even if there is a `fetchPolicy` specified. (fixes #11365) diff --git a/.size-limits.json b/.size-limits.json index 5cca69e7256..c9a1233d358 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39906, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32896 + "dist/apollo-client.min.cjs": 39924, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32903 } diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index f9e6dd6b1e4..7a419ff078e 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -910,7 +910,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, options.fetchPolicy !== "standby" && // If we're changing the fetchPolicy anyway, don't try to change it here // using applyNextFetchPolicy. The explicit options.fetchPolicy wins. - options.fetchPolicy === oldFetchPolicy + (options.fetchPolicy === oldFetchPolicy || + // A `nextFetchPolicy` function has even higher priority, though, + // so in that case `applyNextFetchPolicy` must be called. + typeof options.nextFetchPolicy === "function") ) { this.applyNextFetchPolicy("variables-changed", options); if (newNetworkStatus === void 0) { diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 7909f8673d8..19a1ba57687 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -11,6 +11,7 @@ import { OperationVariables, TypedDocumentNode, WatchQueryFetchPolicy, + WatchQueryOptions, } from "../../../core"; import { InMemoryCache } from "../../../cache"; import { ApolloProvider } from "../../context"; @@ -36,6 +37,7 @@ import { } from "../../../testing/internal"; import { useApolloClient } from "../useApolloClient"; import { useLazyQuery } from "../useLazyQuery"; +import { mockFetchQuery } from "../../../core/__tests__/ObservableQuery"; const IS_REACT_17 = React.version.startsWith("17"); @@ -7071,6 +7073,131 @@ describe("useQuery Hook", () => { expect(reasons).toEqual(["variables-changed", "after-fetch"]); }); + + it("should prioritize a `nextFetchPolicy` function over a `fetchPolicy` option when changing variables", async () => { + const query = gql` + { + hello + } + `; + const link = new MockLink([ + { + request: { query, variables: { id: 1 } }, + result: { data: { hello: "from link" } }, + delay: 10, + }, + { + request: { query, variables: { id: 2 } }, + result: { data: { hello: "from link2" } }, + delay: 10, + }, + ]); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link, + }); + + const mocks = mockFetchQuery(client["queryManager"]); + + const expectQueryTriggered = ( + nth: number, + fetchPolicy: WatchQueryFetchPolicy + ) => { + expect(mocks.fetchQueryByPolicy).toHaveBeenCalledTimes(nth); + expect(mocks.fetchQueryByPolicy).toHaveBeenNthCalledWith( + nth, + expect.anything(), + expect.objectContaining({ fetchPolicy }), + expect.any(Number) + ); + }; + let nextFetchPolicy: WatchQueryOptions< + OperationVariables, + any + >["nextFetchPolicy"] = (_, context) => { + if (context.reason === "variables-changed") { + return "cache-and-network"; + } else if (context.reason === "after-fetch") { + return "cache-only"; + } + throw new Error("should never happen"); + }; + nextFetchPolicy = jest.fn(nextFetchPolicy); + + const { result, rerender } = renderHook< + QueryResult, + { + variables: { id: number }; + } + >( + ({ variables }) => + useQuery(query, { + fetchPolicy: "network-only", + variables, + notifyOnNetworkStatusChange: true, + nextFetchPolicy, + }), + { + initialProps: { + variables: { id: 1 }, + }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + // first network request triggers with initial fetchPolicy + expectQueryTriggered(1, "network-only"); + + await waitFor(() => { + expect(result.current.networkStatus).toBe(NetworkStatus.ready); + }); + + expect(nextFetchPolicy).toHaveBeenCalledTimes(1); + expect(nextFetchPolicy).toHaveBeenNthCalledWith( + 1, + "network-only", + expect.objectContaining({ + reason: "after-fetch", + }) + ); + // `nextFetchPolicy(..., {reason: "after-fetch"})` changed it to + // cache-only + expect(result.current.observable.options.fetchPolicy).toBe("cache-only"); + + rerender({ + variables: { id: 2 }, + }); + + expect(nextFetchPolicy).toHaveBeenNthCalledWith( + 2, + // has been reset to the initial `fetchPolicy` of "network-only" because + // we changed variables, then `nextFetchPolicy` is called + "network-only", + expect.objectContaining({ + reason: "variables-changed", + }) + ); + // the return value of `nextFetchPolicy(..., {reason: "variables-changed"})` + expectQueryTriggered(2, "cache-and-network"); + + await waitFor(() => { + expect(result.current.networkStatus).toBe(NetworkStatus.ready); + }); + + expect(nextFetchPolicy).toHaveBeenCalledTimes(3); + expect(nextFetchPolicy).toHaveBeenNthCalledWith( + 3, + "cache-and-network", + expect.objectContaining({ + reason: "after-fetch", + }) + ); + // `nextFetchPolicy(..., {reason: "after-fetch"})` changed it to + // cache-only + expect(result.current.observable.options.fetchPolicy).toBe("cache-only"); + }); }); describe("Missing Fields", () => { diff --git a/src/tsconfig.json b/src/tsconfig.json index efeb2f2da38..d7e90510ecc 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -5,6 +5,8 @@ { "compilerOptions": { "noEmit": true, + "declaration": false, + "declarationMap": false, "lib": ["es2015", "esnext.asynciterable", "ES2021.WeakRef"], "types": ["jest", "node", "./testing/matchers/index.d.ts"] }, From 2941824dd66cdd20eee5f2293373ad7a9cf991a4 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 8 Jul 2024 11:38:11 +0200 Subject: [PATCH 41/62] Add `restart` function to `useSubscription` (#11927) * syntax adjustment for compiler * Add `restart` function to `useSubscription`. * add tests * adjust test timing to accomodate for React 17 * Apply suggestions from code review Co-authored-by: Jerel Miller * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: Jerel Miller Co-authored-by: phryneas --- .api-reports/api-report-react.api.md | 8 +- .api-reports/api-report-react_hooks.api.md | 8 +- .api-reports/api-report.api.md | 8 +- .changeset/clever-bikes-admire.md | 5 + .size-limits.json | 2 +- .../hooks/__tests__/useSubscription.test.tsx | 337 +++++++++++++++++- src/react/hooks/useSubscription.ts | 48 ++- 7 files changed, 394 insertions(+), 22 deletions(-) create mode 100644 .changeset/clever-bikes-admire.md diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index c81d938a887..d1cd5a93af3 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -2190,7 +2190,13 @@ export interface UseReadQueryResult { } // @public -export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): SubscriptionResult; +export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): { + restart(): void; + loading: boolean; + data?: TData | undefined; + error?: ApolloError; + variables?: TVariables | undefined; +}; // @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>; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index b5ee3e1c051..cec9f2c4d1e 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -2023,7 +2023,13 @@ export interface UseReadQueryResult { // Warning: (ae-forgotten-export) The symbol "SubscriptionHookOptions" needs to be exported by the entry point index.d.ts // // @public -export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): SubscriptionResult; +export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): { + restart(): void; + loading: boolean; + data?: TData | undefined; + error?: ApolloError; + variables?: TVariables | undefined; +}; // Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookOptions" 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 fb2bec58947..11e76dd8d6f 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2851,7 +2851,13 @@ export interface UseReadQueryResult { } // @public -export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): SubscriptionResult; +export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer_2>): { + restart(): void; + loading: boolean; + data?: TData | undefined; + error?: ApolloError; + variables?: TVariables | undefined; +}; // @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>; diff --git a/.changeset/clever-bikes-admire.md b/.changeset/clever-bikes-admire.md new file mode 100644 index 00000000000..36b9ba5de3a --- /dev/null +++ b/.changeset/clever-bikes-admire.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Add `restart` function to `useSubscription`. diff --git a/.size-limits.json b/.size-limits.json index c9a1233d358..4e756f84c34 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39924, + "dist/apollo-client.min.cjs": 39971, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32903 } diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index decdd17b973..e955ae1e00c 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { renderHook, waitFor } from "@testing-library/react"; +import { render, renderHook, waitFor } from "@testing-library/react"; import gql from "graphql-tag"; import { @@ -14,7 +14,10 @@ import { InMemoryCache as Cache } from "../../../cache"; import { ApolloProvider } from "../../context"; import { MockSubscriptionLink } from "../../../testing"; import { useSubscription } from "../useSubscription"; -import { spyOnConsole } from "../../../testing/internal"; +import { profileHook, spyOnConsole } from "../../../testing/internal"; +import { SubscriptionHookOptions } from "../../types/types"; +import { GraphQLError } from "graphql"; +import { InvariantError } from "ts-invariant"; describe("useSubscription Hook", () => { it("should handle a simple subscription properly", async () => { @@ -1122,6 +1125,336 @@ followed by new in-flight setup", async () => { }); }); +describe("`restart` callback", () => { + function setup() { + const subscription: TypedDocumentNode< + { totalLikes: number }, + { id: string } + > = gql` + subscription ($id: ID!) { + totalLikes(postId: $id) + } + `; + const onSubscribe = jest.fn(); + const onUnsubscribe = jest.fn(); + const link = new MockSubscriptionLink(); + link.onSetup(onSubscribe); + link.onUnsubscribe(onUnsubscribe); + const client = new ApolloClient({ + link, + cache: new Cache(), + }); + const ProfiledHook = profileHook( + ( + options: SubscriptionHookOptions<{ totalLikes: number }, { id: string }> + ) => useSubscription(subscription, options) + ); + return { client, link, ProfiledHook, onSubscribe, onUnsubscribe }; + } + it("can restart a running subscription", async () => { + const { client, link, ProfiledHook, onSubscribe, onUnsubscribe } = setup(); + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: true, + data: undefined, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + link.simulateResult({ result: { data: { totalLikes: 1 } } }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + data: { totalLikes: 1 }, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + await expect(ProfiledHook).not.toRerender({ timeout: 20 }); + expect(onUnsubscribe).toHaveBeenCalledTimes(0); + expect(onSubscribe).toHaveBeenCalledTimes(1); + + ProfiledHook.getCurrentSnapshot().restart(); + + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: true, + data: undefined, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + await waitFor(() => expect(onUnsubscribe).toHaveBeenCalledTimes(1)); + expect(onSubscribe).toHaveBeenCalledTimes(2); + + link.simulateResult({ result: { data: { totalLikes: 2 } } }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + data: { totalLikes: 2 }, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + }); + it("will use the most recently passed in options", async () => { + const { client, link, ProfiledHook, onSubscribe, onUnsubscribe } = setup(); + const { rerender } = render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: true, + data: undefined, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + // deliberately keeping a reference to a very old `restart` function + // to show that the most recent options are used even with that + const restart = ProfiledHook.getCurrentSnapshot().restart; + link.simulateResult({ result: { data: { totalLikes: 1 } } }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + data: { totalLikes: 1 }, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + await expect(ProfiledHook).not.toRerender({ timeout: 20 }); + expect(onUnsubscribe).toHaveBeenCalledTimes(0); + expect(onSubscribe).toHaveBeenCalledTimes(1); + + rerender(); + await waitFor(() => expect(onUnsubscribe).toHaveBeenCalledTimes(1)); + expect(onSubscribe).toHaveBeenCalledTimes(2); + expect(link.operation?.variables).toStrictEqual({ id: "2" }); + + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: true, + data: undefined, + error: undefined, + restart: expect.any(Function), + variables: { id: "2" }, + }); + } + link.simulateResult({ result: { data: { totalLikes: 1000 } } }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + data: { totalLikes: 1000 }, + error: undefined, + restart: expect.any(Function), + variables: { id: "2" }, + }); + } + + expect(onUnsubscribe).toHaveBeenCalledTimes(1); + expect(onSubscribe).toHaveBeenCalledTimes(2); + expect(link.operation?.variables).toStrictEqual({ id: "2" }); + + restart(); + + await waitFor(() => expect(onUnsubscribe).toHaveBeenCalledTimes(2)); + expect(onSubscribe).toHaveBeenCalledTimes(3); + expect(link.operation?.variables).toStrictEqual({ id: "2" }); + + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: true, + data: undefined, + error: undefined, + restart: expect.any(Function), + variables: { id: "2" }, + }); + } + link.simulateResult({ result: { data: { totalLikes: 1005 } } }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + data: { totalLikes: 1005 }, + error: undefined, + restart: expect.any(Function), + variables: { id: "2" }, + }); + } + }); + it("can restart a subscription that has completed", async () => { + const { client, link, ProfiledHook, onSubscribe, onUnsubscribe } = setup(); + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: true, + data: undefined, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + link.simulateResult({ result: { data: { totalLikes: 1 } } }, true); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + data: { totalLikes: 1 }, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + await expect(ProfiledHook).not.toRerender({ timeout: 20 }); + expect(onUnsubscribe).toHaveBeenCalledTimes(1); + expect(onSubscribe).toHaveBeenCalledTimes(1); + + ProfiledHook.getCurrentSnapshot().restart(); + + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: true, + data: undefined, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + await waitFor(() => expect(onSubscribe).toHaveBeenCalledTimes(2)); + expect(onUnsubscribe).toHaveBeenCalledTimes(1); + + link.simulateResult({ result: { data: { totalLikes: 2 } } }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + data: { totalLikes: 2 }, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + }); + it("can restart a subscription that has errored", async () => { + const { client, link, ProfiledHook, onSubscribe, onUnsubscribe } = setup(); + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: true, + data: undefined, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + const error = new GraphQLError("error"); + link.simulateResult({ + result: { errors: [error] }, + }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + data: undefined, + error: new ApolloError({ graphQLErrors: [error] }), + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + await expect(ProfiledHook).not.toRerender({ timeout: 20 }); + expect(onUnsubscribe).toHaveBeenCalledTimes(1); + expect(onSubscribe).toHaveBeenCalledTimes(1); + + ProfiledHook.getCurrentSnapshot().restart(); + + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: true, + data: undefined, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + await waitFor(() => expect(onSubscribe).toHaveBeenCalledTimes(2)); + expect(onUnsubscribe).toHaveBeenCalledTimes(1); + + link.simulateResult({ result: { data: { totalLikes: 2 } } }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + data: { totalLikes: 2 }, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + }); + it("will not restart a subscription that has been `skip`ped", async () => { + const { client, ProfiledHook, onSubscribe, onUnsubscribe } = setup(); + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + data: undefined, + error: undefined, + restart: expect.any(Function), + variables: { id: "1" }, + }); + } + expect(onUnsubscribe).toHaveBeenCalledTimes(0); + expect(onSubscribe).toHaveBeenCalledTimes(0); + + expect(() => ProfiledHook.getCurrentSnapshot().restart()).toThrow( + new InvariantError("A subscription that is skipped cannot be restarted.") + ); + + await expect(ProfiledHook).not.toRerender({ timeout: 20 }); + expect(onUnsubscribe).toHaveBeenCalledTimes(0); + expect(onSubscribe).toHaveBeenCalledTimes(0); + }); +}); + describe.skip("Type Tests", () => { test("NoInfer prevents adding arbitrary additional variables", () => { const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>; diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts index 3b0d7f4303d..b1f3a9a733b 100644 --- a/src/react/hooks/useSubscription.ts +++ b/src/react/hooks/useSubscription.ts @@ -21,6 +21,7 @@ import { Observable } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { useDeepMemo } from "./internal/useDeepMemo.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; +import { useIsomorphicLayoutEffect } from "./internal/useIsomorphicLayoutEffect.js"; /** * > Refer to the [Subscriptions](https://www.apollographql.com/docs/react/data/subscriptions/) section for a more in-depth overview of `useSubscription`. @@ -146,6 +147,14 @@ export function useSubscription< ) ); + const recreate = () => + createSubscription(client, subscription, variables, fetchPolicy, context); + + const recreateRef = React.useRef(recreate); + useIsomorphicLayoutEffect(() => { + recreateRef.current = recreate; + }); + if (skip) { if (observable) { setObservable((observable = null)); @@ -160,15 +169,7 @@ export function useSubscription< !!shouldResubscribe(options!) : shouldResubscribe) !== false) ) { - setObservable( - (observable = createSubscription( - client, - subscription, - variables, - fetchPolicy, - context - )) - ); + setObservable((observable = recreate())); } const optionsRef = React.useRef(options); @@ -186,7 +187,7 @@ export function useSubscription< [skip, variables] ); - return useSyncExternalStore>( + const ret = useSyncExternalStore>( React.useCallback( (update) => { if (!observable) { @@ -262,6 +263,19 @@ export function useSubscription< ), () => (observable && !skip ? observable.__.result : fallbackResult) ); + return React.useMemo( + () => ({ + ...ret, + restart() { + invariant( + !optionsRef.current.skip, + "A subscription that is skipped cannot be restarted." + ); + setObservable(recreateRef.current()); + }, + }), + [ret] + ); } function createSubscription< @@ -295,12 +309,14 @@ function createSubscription< new Observable>((observer) => { // lazily start the subscription when the first observer subscribes // to get around strict mode - observable ||= client.subscribe({ - query, - variables, - fetchPolicy, - context, - }); + if (!observable) { + observable = client.subscribe({ + query, + variables, + fetchPolicy, + context, + }); + } const sub = observable.subscribe(observer); return () => sub.unsubscribe(); }), From 70406bfd2b9a645d781638569853d9b435e047df Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 8 Jul 2024 13:04:26 +0200 Subject: [PATCH 42/62] add `ignoreResults` option to `useSubscription` (#11921) * add `ignoreResults` option to `useSubscription` * more tests * changeset * restore type, add deprecation, tweak tag * Update src/react/hooks/useSubscription.ts * reflect code change in comment * review feedback * Update src/react/types/types.documentation.ts Co-authored-by: Jerel Miller * add clarification about resetting the return value when switching on `ignoreResults` later * test fixup --------- Co-authored-by: Jerel Miller --- .api-reports/api-report-react.api.md | 3 +- .../api-report-react_components.api.md | 1 + .api-reports/api-report-react_hooks.api.md | 1 + .api-reports/api-report.api.md | 3 +- .changeset/unlucky-birds-press.md | 5 + .size-limits.json | 2 +- .../hooks/__tests__/useSubscription.test.tsx | 291 ++++++++++++++++++ src/react/hooks/useSubscription.ts | 30 +- src/react/types/types.documentation.ts | 6 + src/react/types/types.ts | 8 + 10 files changed, 341 insertions(+), 9 deletions(-) create mode 100644 .changeset/unlucky-birds-press.md diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index d1cd5a93af3..d685c3f6599 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -388,6 +388,7 @@ export interface BaseSubscriptionOptions void; onData?: (options: OnDataOptions) => any; onError?: (error: ApolloError) => void; @@ -1919,7 +1920,7 @@ export interface SubscriptionCurrentObservable { subscription?: Subscription; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface SubscriptionDataOptions extends BaseSubscriptionOptions { // (undocumented) children?: null | ((result: SubscriptionResult) => ReactTypes.ReactNode); diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index efb4ddab230..0aff1af40cf 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -336,6 +336,7 @@ interface BaseSubscriptionOptions void; // Warning: (ae-forgotten-export) The symbol "OnDataOptions" needs to be exported by the entry point index.d.ts onData?: (options: OnDataOptions) => any; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index cec9f2c4d1e..2dcd7bce461 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -359,6 +359,7 @@ interface BaseSubscriptionOptions void; // Warning: (ae-forgotten-export) The symbol "OnDataOptions" needs to be exported by the entry point index.d.ts onData?: (options: OnDataOptions) => any; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 11e76dd8d6f..007a3ba589b 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -359,6 +359,7 @@ export interface BaseSubscriptionOptions; context?: DefaultContext; fetchPolicy?: FetchPolicy; + ignoreResults?: boolean; onComplete?: () => void; onData?: (options: OnDataOptions) => any; onError?: (error: ApolloError) => void; @@ -2551,7 +2552,7 @@ export interface SubscriptionCurrentObservable { subscription?: ObservableSubscription; } -// @public (undocumented) +// @public @deprecated (undocumented) export interface SubscriptionDataOptions extends BaseSubscriptionOptions { // (undocumented) children?: null | ((result: SubscriptionResult) => ReactTypes.ReactNode); diff --git a/.changeset/unlucky-birds-press.md b/.changeset/unlucky-birds-press.md new file mode 100644 index 00000000000..5696787576d --- /dev/null +++ b/.changeset/unlucky-birds-press.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +add `ignoreResults` option to `useSubscription` diff --git a/.size-limits.json b/.size-limits.json index 4e756f84c34..28452c40fd4 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39971, + "dist/apollo-client.min.cjs": 40015, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32903 } diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index e955ae1e00c..eb02e41c9aa 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -1455,6 +1455,297 @@ describe("`restart` callback", () => { }); }); +describe("ignoreResults", () => { + const subscription = gql` + subscription { + car { + make + } + } + `; + + const results = ["Audi", "BMW"].map((make) => ({ + result: { data: { car: { make } } }, + })); + + it("should not rerender when ignoreResults is true, but will call `onData` and `onComplete`", async () => { + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + link, + cache: new Cache({ addTypename: false }), + }); + + const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]); + const onError = jest.fn((() => {}) as SubscriptionHookOptions["onError"]); + const onComplete = jest.fn( + (() => {}) as SubscriptionHookOptions["onComplete"] + ); + const ProfiledHook = profileHook(() => + useSubscription(subscription, { + ignoreResults: true, + onData, + onError, + onComplete, + }) + ); + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + error: undefined, + data: undefined, + variables: undefined, + restart: expect.any(Function), + }); + link.simulateResult(results[0]); + + await waitFor(() => { + expect(onData).toHaveBeenCalledTimes(1); + expect(onData).toHaveBeenLastCalledWith( + expect.objectContaining({ + data: { + data: results[0].result.data, + error: undefined, + loading: false, + variables: undefined, + }, + }) + ); + expect(onError).toHaveBeenCalledTimes(0); + expect(onComplete).toHaveBeenCalledTimes(0); + }); + + link.simulateResult(results[1], true); + await waitFor(() => { + expect(onData).toHaveBeenCalledTimes(2); + expect(onData).toHaveBeenLastCalledWith( + expect.objectContaining({ + data: { + data: results[1].result.data, + error: undefined, + loading: false, + variables: undefined, + }, + }) + ); + expect(onError).toHaveBeenCalledTimes(0); + expect(onComplete).toHaveBeenCalledTimes(1); + }); + + await expect(ProfiledHook).not.toRerender(); + }); + + it("should not rerender when ignoreResults is true and an error occurs", async () => { + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + link, + cache: new Cache({ addTypename: false }), + }); + + const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]); + const onError = jest.fn((() => {}) as SubscriptionHookOptions["onError"]); + const onComplete = jest.fn( + (() => {}) as SubscriptionHookOptions["onComplete"] + ); + const ProfiledHook = profileHook(() => + useSubscription(subscription, { + ignoreResults: true, + onData, + onError, + onComplete, + }) + ); + render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + error: undefined, + data: undefined, + variables: undefined, + restart: expect.any(Function), + }); + link.simulateResult(results[0]); + + await waitFor(() => { + expect(onData).toHaveBeenCalledTimes(1); + expect(onData).toHaveBeenLastCalledWith( + expect.objectContaining({ + data: { + data: results[0].result.data, + error: undefined, + loading: false, + variables: undefined, + }, + }) + ); + expect(onError).toHaveBeenCalledTimes(0); + expect(onComplete).toHaveBeenCalledTimes(0); + }); + + const error = new Error("test"); + link.simulateResult({ error }); + await waitFor(() => { + expect(onData).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenLastCalledWith(error); + expect(onComplete).toHaveBeenCalledTimes(0); + }); + + await expect(ProfiledHook).not.toRerender(); + }); + + it("can switch from `ignoreResults: true` to `ignoreResults: false` and will start rerendering, without creating a new subscription", async () => { + const subscriptionCreated = jest.fn(); + const link = new MockSubscriptionLink(); + link.onSetup(subscriptionCreated); + const client = new ApolloClient({ + link, + cache: new Cache({ addTypename: false }), + }); + + const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]); + const ProfiledHook = profileHook( + ({ ignoreResults }: { ignoreResults: boolean }) => + useSubscription(subscription, { + ignoreResults, + onData, + }) + ); + const { rerender } = render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + expect(subscriptionCreated).toHaveBeenCalledTimes(1); + + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + error: undefined, + data: undefined, + variables: undefined, + restart: expect.any(Function), + }); + expect(onData).toHaveBeenCalledTimes(0); + } + link.simulateResult(results[0]); + await expect(ProfiledHook).not.toRerender({ timeout: 20 }); + expect(onData).toHaveBeenCalledTimes(1); + + rerender(); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + error: undefined, + // `data` appears immediately after changing to `ignoreResults: false` + data: results[0].result.data, + variables: undefined, + restart: expect.any(Function), + }); + // `onData` should not be called again for the same result + expect(onData).toHaveBeenCalledTimes(1); + } + + link.simulateResult(results[1]); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + error: undefined, + data: results[1].result.data, + variables: undefined, + restart: expect.any(Function), + }); + expect(onData).toHaveBeenCalledTimes(2); + } + // a second subscription should not have been started + expect(subscriptionCreated).toHaveBeenCalledTimes(1); + }); + it("can switch from `ignoreResults: false` to `ignoreResults: true` and will stop rerendering, without creating a new subscription", async () => { + const subscriptionCreated = jest.fn(); + const link = new MockSubscriptionLink(); + link.onSetup(subscriptionCreated); + const client = new ApolloClient({ + link, + cache: new Cache({ addTypename: false }), + }); + + const onData = jest.fn((() => {}) as SubscriptionHookOptions["onData"]); + const ProfiledHook = profileHook( + ({ ignoreResults }: { ignoreResults: boolean }) => + useSubscription(subscription, { + ignoreResults, + onData, + }) + ); + const { rerender } = render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + expect(subscriptionCreated).toHaveBeenCalledTimes(1); + + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: true, + error: undefined, + data: undefined, + variables: undefined, + restart: expect.any(Function), + }); + expect(onData).toHaveBeenCalledTimes(0); + } + link.simulateResult(results[0]); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + error: undefined, + data: results[0].result.data, + variables: undefined, + restart: expect.any(Function), + }); + expect(onData).toHaveBeenCalledTimes(1); + } + await expect(ProfiledHook).not.toRerender({ timeout: 20 }); + + rerender(); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + error: undefined, + // switching back to the default `ignoreResults: true` return value + data: undefined, + variables: undefined, + restart: expect.any(Function), + }); + // `onData` should not be called again + expect(onData).toHaveBeenCalledTimes(1); + } + + link.simulateResult(results[1]); + await expect(ProfiledHook).not.toRerender({ timeout: 20 }); + expect(onData).toHaveBeenCalledTimes(2); + + // a second subscription should not have been started + expect(subscriptionCreated).toHaveBeenCalledTimes(1); + }); +}); + describe.skip("Type Tests", () => { test("NoInfer prevents adding arbitrary additional variables", () => { const typedNode = {} as TypedDocumentNode<{ foo: string }, { bar: number }>; diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts index b1f3a9a733b..d602578de73 100644 --- a/src/react/hooks/useSubscription.ts +++ b/src/react/hooks/useSubscription.ts @@ -138,7 +138,8 @@ export function useSubscription< } } - const { skip, fetchPolicy, shouldResubscribe, context } = options; + const { skip, fetchPolicy, shouldResubscribe, context, ignoreResults } = + options; const variables = useDeepMemo(() => options.variables, [options.variables]); let [observable, setObservable] = React.useState(() => @@ -177,16 +178,30 @@ export function useSubscription< optionsRef.current = options; }); + const fallbackLoading = !skip && !ignoreResults; const fallbackResult = React.useMemo>( () => ({ - loading: !skip, + loading: fallbackLoading, error: void 0, data: void 0, variables, }), - [skip, variables] + [fallbackLoading, variables] ); + const ignoreResultsRef = React.useRef(ignoreResults); + useIsomorphicLayoutEffect(() => { + // We cannot reference `ignoreResults` directly in the effect below + // it would add a dependency to the `useEffect` deps array, which means the + // subscription would be recreated if `ignoreResults` changes + // As a result, on resubscription, the last result would be re-delivered, + // rendering the component one additional time, and re-triggering `onData`. + // The same applies to `fetchPolicy`, which results in a new `observable` + // being created. We cannot really avoid it in that case, but we can at least + // avoid it for `ignoreResults`. + ignoreResultsRef.current = ignoreResults; + }); + const ret = useSyncExternalStore>( React.useCallback( (update) => { @@ -212,7 +227,7 @@ export function useSubscription< variables, }; observable.__.setResult(result); - update(); + if (!ignoreResultsRef.current) update(); if (optionsRef.current.onData) { optionsRef.current.onData({ @@ -234,7 +249,7 @@ export function useSubscription< error, variables, }); - update(); + if (!ignoreResultsRef.current) update(); optionsRef.current.onError?.(error); } }, @@ -261,7 +276,10 @@ export function useSubscription< }, [observable] ), - () => (observable && !skip ? observable.__.result : fallbackResult) + () => + observable && !skip && !ignoreResults ? + observable.__.result + : fallbackResult ); return React.useMemo( () => ({ diff --git a/src/react/types/types.documentation.ts b/src/react/types/types.documentation.ts index 186d651dfd8..c5f232c1b18 100644 --- a/src/react/types/types.documentation.ts +++ b/src/react/types/types.documentation.ts @@ -531,6 +531,12 @@ export interface SubscriptionOptionsDocumentation { */ shouldResubscribe: unknown; + /** + * If `true`, the hook will not cause the component to rerender. This is useful when you want to control the rendering of your component yourself with logic in the `onData` and `onError` callbacks. + * + * Changing this to `true` when the hook already has `data` will reset the `data` to `undefined`. + */ + ignoreResults: unknown; /** * An `ApolloClient` instance. By default `useSubscription` / `Subscription` uses the client passed down via context, but a different client can be passed in. */ diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 41cff9e8835..be799bf52dd 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -457,6 +457,11 @@ export interface BaseSubscriptionOptions< onError?: (error: ApolloError) => void; /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onSubscriptionComplete:member} */ onSubscriptionComplete?: () => void; + /** + * {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#ignoreResults:member} + * @defaultValue `false` + */ + ignoreResults?: boolean; } export interface SubscriptionResult { @@ -479,6 +484,9 @@ export interface SubscriptionHookOptions< TVariables extends OperationVariables = OperationVariables, > extends BaseSubscriptionOptions {} +/** + * @deprecated This type is not used anymore. It will be removed in the next major version of Apollo Client + */ export interface SubscriptionDataOptions< TData = any, TVariables extends OperationVariables = OperationVariables, From 09a6677ec1a0cffedeecb2cbac5cd3a3c8aa0fa1 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 9 Jul 2024 09:39:27 +0200 Subject: [PATCH 43/62] also wrap createQueryPreloader (#11719) * also wrap `createQueryPreloader` * changeset * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: phryneas --- .api-reports/api-report-react_internal.api.md | 72 ++++++++++++++++++- .changeset/thin-lies-begin.md | 5 ++ .size-limits.json | 2 +- src/react/hooks/internal/wrapHook.ts | 2 + .../query-preloader/createQueryPreloader.ts | 11 ++- 5 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 .changeset/thin-lies-begin.md diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 95a7366593c..77a4d29c2f8 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -493,6 +493,11 @@ class Concast extends Observable { // @public (undocumented) type ConcastSourcesIterable = Iterable>; +// Warning: (ae-forgotten-export) The symbol "PreloadQueryFunction" needs to be exported by the entry point index.d.ts +// +// @public +function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -1252,6 +1257,11 @@ const OBSERVED_CHANGED_OPTIONS: readonly ["canonizeResults", "context", "errorPo // @public (undocumented) type ObservedOptions = Pick; +// @public +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1291,6 +1301,50 @@ export interface PreloadedQueryRef extend toPromise(): Promise>; } +// @public (undocumented) +type PreloadQueryFetchPolicy = Extract; + +// @public +interface PreloadQueryFunction { + // Warning: (ae-forgotten-export) The symbol "PreloadQueryOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "PreloadQueryOptionsArg" needs to be exported by the entry point index.d.ts + >(query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg, TOptions>): PreloadedQueryRef | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + }): PreloadedQueryRef | undefined, TVariables>; + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + errorPolicy: "ignore" | "all"; + }): PreloadedQueryRef; + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + returnPartialData: true; + }): PreloadedQueryRef, TVariables>; + (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) +type PreloadQueryOptions = { + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: PreloadQueryFetchPolicy; + returnPartialData?: boolean; + refetchWritePolicy?: RefetchWritePolicy; +} & VariablesOption; + +// Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type PreloadQueryOptionsArg = [TVariables] extends [never] ? [ +options?: PreloadQueryOptions & TOptions +] : {} extends OnlyRequiredProperties ? [ +options?: PreloadQueryOptions> & Omit +] : [ +options: PreloadQueryOptions> & Omit +]; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1692,7 +1746,6 @@ interface SharedWatchQueryOptions // @deprecated partialRefetch?: boolean; pollInterval?: number; - // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts refetchWritePolicy?: RefetchWritePolicy; returnPartialData?: boolean; skipPollAttempt?: () => boolean; @@ -2049,6 +2102,17 @@ interface UseSuspenseQueryResult; } +// @public (undocumented) +type VariablesOption = [ +TVariables +] extends [never] ? { + variables?: Record; +} : {} extends OnlyRequiredProperties ? { + variables?: TVariables; +} : { + variables: TVariables; +}; + // @public interface WatchFragmentOptions { // @deprecated (undocumented) @@ -2081,6 +2145,10 @@ interface WatchQueryOptions(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/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 // (No @packageDocumentation comment for this package) diff --git a/.changeset/thin-lies-begin.md b/.changeset/thin-lies-begin.md new file mode 100644 index 00000000000..bc258c65cdf --- /dev/null +++ b/.changeset/thin-lies-begin.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Allow wrapping `createQueryPreloader` diff --git a/.size-limits.json b/.size-limits.json index 28452c40fd4..e794acb3beb 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 40015, + "dist/apollo-client.min.cjs": 40030, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32903 } diff --git a/src/react/hooks/internal/wrapHook.ts b/src/react/hooks/internal/wrapHook.ts index c22ec726e9d..59b112c3216 100644 --- a/src/react/hooks/internal/wrapHook.ts +++ b/src/react/hooks/internal/wrapHook.ts @@ -9,10 +9,12 @@ import type { import type { QueryManager } from "../../../core/QueryManager.js"; import type { ApolloClient } from "../../../core/ApolloClient.js"; import type { ObservableQuery } from "../../../core/ObservableQuery.js"; +import type { createQueryPreloader } from "../../query-preloader/createQueryPreloader.js"; const wrapperSymbol = Symbol.for("apollo.hook.wrappers"); interface WrappableHooks { + createQueryPreloader: typeof createQueryPreloader; useQuery: typeof useQuery; useSuspenseQuery: typeof useSuspenseQuery; useBackgroundQuery: typeof useBackgroundQuery; diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 226723dab9a..6389992519c 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -16,6 +16,7 @@ import type { import { InternalQueryReference, wrapQueryRef } from "../internal/index.js"; import type { PreloadedQueryRef } from "../internal/index.js"; import type { NoInfer } from "../index.js"; +import { wrapHook } from "../hooks/internal/index.js"; type VariablesOption = [TVariables] extends [never] ? @@ -168,6 +169,14 @@ export interface PreloadQueryFunction { export function createQueryPreloader( client: ApolloClient ): PreloadQueryFunction { + return wrapHook( + "createQueryPreloader", + _createQueryPreloader, + client + )(client); +} + +const _createQueryPreloader: typeof createQueryPreloader = (client) => { return function preloadQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables, @@ -189,4 +198,4 @@ export function createQueryPreloader( return wrapQueryRef(queryRef) as PreloadedQueryRef; }; -} +}; From 602a67896886c9ce8595d340fe88c3768a79b4d2 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 9 Jul 2024 15:00:50 +0200 Subject: [PATCH 44/62] forward `errorPolicy` option from `useSubscription` (#11928) * forward `errorPolicy` option from `useSubscription` * more work * add test * ensure errors are instanceOf `ApolloError` * test with more realistic errors * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: phryneas --- .api-reports/api-report-react.api.md | 3 +- .../api-report-react_components.api.md | 3 +- .api-reports/api-report-react_hooks.api.md | 3 +- .api-reports/api-report.api.md | 1 + .size-limits.json | 2 +- .../hooks/__tests__/useSubscription.test.tsx | 248 +++++++++++++++++- src/react/hooks/useQuery.ts | 4 +- src/react/hooks/useSubscription.ts | 46 +++- src/react/types/types.ts | 2 + 9 files changed, 298 insertions(+), 14 deletions(-) diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index d685c3f6599..6fd83bb10f4 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -386,6 +386,8 @@ export interface BaseQueryOptions { client?: ApolloClient; context?: Context; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + errorPolicy?: ErrorPolicy; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts fetchPolicy?: FetchPolicy; ignoreResults?: boolean; @@ -994,7 +996,6 @@ export interface LoadableQueryHookOptions { canonizeResults?: boolean; client?: ApolloClient; context?: Context; - // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts errorPolicy?: ErrorPolicy; fetchPolicy?: LoadableQueryHookFetchPolicy; queryKey?: string | number | any[]; diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index 0aff1af40cf..350691ae935 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -334,6 +334,8 @@ interface BaseQueryOptions { client?: ApolloClient; context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + errorPolicy?: ErrorPolicy; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts fetchPolicy?: FetchPolicy; ignoreResults?: boolean; @@ -982,7 +984,6 @@ export interface Mutation { interface MutationBaseOptions = ApolloCache> { awaitRefetchQueries?: boolean; context?: TContext; - // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts errorPolicy?: ErrorPolicy; // Warning: (ae-forgotten-export) The symbol "OnQueryUpdated" needs to be exported by the entry point index.d.ts onQueryUpdated?: OnQueryUpdated; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index 2dcd7bce461..861d5c89198 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -357,6 +357,8 @@ interface BaseQueryOptions { client?: ApolloClient; context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + errorPolicy?: ErrorPolicy; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts fetchPolicy?: FetchPolicy; ignoreResults?: boolean; @@ -942,7 +944,6 @@ interface LoadableQueryHookOptions { canonizeResults?: boolean; client?: ApolloClient; context?: DefaultContext; - // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts errorPolicy?: ErrorPolicy; // Warning: (ae-forgotten-export) The symbol "LoadableQueryHookFetchPolicy" needs to be exported by the entry point index.d.ts fetchPolicy?: LoadableQueryHookFetchPolicy; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 007a3ba589b..181d29f8273 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -358,6 +358,7 @@ export interface BaseQueryOptions { client?: ApolloClient; context?: DefaultContext; + errorPolicy?: ErrorPolicy; fetchPolicy?: FetchPolicy; ignoreResults?: boolean; onComplete?: () => void; diff --git a/.size-limits.json b/.size-limits.json index e794acb3beb..e7e76549d6e 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 40030, + "dist/apollo-client.min.cjs": 40066, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32903 } diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index eb02e41c9aa..c003585f309 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -16,6 +16,8 @@ import { MockSubscriptionLink } from "../../../testing"; import { useSubscription } from "../useSubscription"; import { profileHook, spyOnConsole } from "../../../testing/internal"; import { SubscriptionHookOptions } from "../../types/types"; +import { ErrorBoundary } from "react-error-boundary"; +import { MockedSubscriptionResult } from "../../../testing/core/mocking/mockSubscriptionLink"; import { GraphQLError } from "graphql"; import { InvariantError } from "ts-invariant"; @@ -1123,6 +1125,248 @@ followed by new in-flight setup", async () => { unmount(); }); + + describe("errorPolicy", () => { + function setup() { + const subscription: TypedDocumentNode<{ totalLikes: number }, {}> = gql` + subscription ($id: ID!) { + totalLikes + } + `; + const errorBoundaryOnError = jest.fn(); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + link, + cache: new Cache(), + }); + const ProfiledHook = profileHook( + (options: SubscriptionHookOptions<{ totalLikes: number }, {}>) => + useSubscription(subscription, options) + ); + const wrapper = ({ children }: { children: any }) => ( + + error}> + {children} + + + ); + const graphQlErrorResult: MockedSubscriptionResult = { + result: { + data: { totalLikes: 42 }, + errors: [{ message: "test" } as any], + }, + }; + const protocolErrorResult: MockedSubscriptionResult = { + error: new Error("Socket closed with event -1: I'm a test!"), + }; + return { + client, + link, + errorBoundaryOnError, + ProfiledHook, + wrapper, + graphQlErrorResult, + protocolErrorResult, + }; + } + describe("GraphQL error", () => { + it.each([undefined, "none"] as const)( + "`errorPolicy: '%s'`: returns `{ error }`, calls `onError`", + async (errorPolicy) => { + const { + ProfiledHook, + wrapper, + link, + graphQlErrorResult, + errorBoundaryOnError, + } = setup(); + const onData = jest.fn(); + const onError = jest.fn(); + render( + , + { + wrapper, + } + ); + + await ProfiledHook.takeSnapshot(); + link.simulateResult(graphQlErrorResult); + { + const snapshot = await ProfiledHook.takeSnapshot(); + console.dir({ graphQlErrorResult, snapshot }, { depth: 5 }); + expect(snapshot).toStrictEqual({ + loading: false, + error: new ApolloError({ + graphQLErrors: graphQlErrorResult.result!.errors as any, + }), + data: undefined, + restart: expect.any(Function), + variables: undefined, + }); + expect(snapshot.error).toBeInstanceOf(ApolloError); + } + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith( + new ApolloError({ + graphQLErrors: graphQlErrorResult.result!.errors as any, + }) + ); + expect(onError).toHaveBeenCalledWith(expect.any(ApolloError)); + expect(onData).toHaveBeenCalledTimes(0); + expect(errorBoundaryOnError).toHaveBeenCalledTimes(0); + } + ); + it("`errorPolicy: 'all'`: returns `{ error, data }`, calls `onError`", async () => { + const { + ProfiledHook, + wrapper, + link, + graphQlErrorResult, + errorBoundaryOnError, + } = setup(); + const onData = jest.fn(); + const onError = jest.fn(); + render( + , + { + wrapper, + } + ); + + await ProfiledHook.takeSnapshot(); + link.simulateResult(graphQlErrorResult); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + error: new ApolloError({ + errorMessage: "test", + graphQLErrors: graphQlErrorResult.result!.errors as any, + }), + data: { totalLikes: 42 }, + restart: expect.any(Function), + variables: undefined, + }); + expect(snapshot.error).toBeInstanceOf(ApolloError); + } + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith( + new ApolloError({ + errorMessage: "test", + graphQLErrors: graphQlErrorResult.result!.errors as any, + }) + ); + expect(onError).toHaveBeenCalledWith(expect.any(ApolloError)); + expect(onData).toHaveBeenCalledTimes(0); + expect(errorBoundaryOnError).toHaveBeenCalledTimes(0); + }); + it("`errorPolicy: 'ignore'`: returns `{ data }`, calls `onData`", async () => { + const { + ProfiledHook, + wrapper, + link, + graphQlErrorResult, + errorBoundaryOnError, + } = setup(); + const onData = jest.fn(); + const onError = jest.fn(); + render( + , + { + wrapper, + } + ); + + await ProfiledHook.takeSnapshot(); + link.simulateResult(graphQlErrorResult); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + error: undefined, + data: { totalLikes: 42 }, + restart: expect.any(Function), + variables: undefined, + }); + } + + expect(onError).toHaveBeenCalledTimes(0); + expect(onData).toHaveBeenCalledTimes(1); + expect(onData).toHaveBeenCalledWith({ + client: expect.anything(), + data: { + data: { totalLikes: 42 }, + loading: false, + // should this be undefined? + error: undefined, + variables: undefined, + }, + }); + expect(errorBoundaryOnError).toHaveBeenCalledTimes(0); + }); + }); + describe("protocol error", () => { + it.each([undefined, "none", "all", "ignore"] as const)( + "`errorPolicy: '%s'`: returns `{ error }`, calls `onError`", + async (errorPolicy) => { + const { + ProfiledHook, + wrapper, + link, + protocolErrorResult, + errorBoundaryOnError, + } = setup(); + const onData = jest.fn(); + const onError = jest.fn(); + render( + , + { + wrapper, + } + ); + + await ProfiledHook.takeSnapshot(); + link.simulateResult(protocolErrorResult); + { + const snapshot = await ProfiledHook.takeSnapshot(); + expect(snapshot).toStrictEqual({ + loading: false, + error: new ApolloError({ + protocolErrors: [protocolErrorResult.error!], + }), + data: undefined, + restart: expect.any(Function), + variables: undefined, + }); + expect(snapshot.error).toBeInstanceOf(ApolloError); + } + + expect(onError).toHaveBeenCalledTimes(1); + expect(onError).toHaveBeenCalledWith(expect.any(ApolloError)); + expect(onError).toHaveBeenCalledWith( + new ApolloError({ + protocolErrors: [protocolErrorResult.error!], + }) + ); + expect(onData).toHaveBeenCalledTimes(0); + expect(errorBoundaryOnError).toHaveBeenCalledTimes(0); + } + ); + }); + }); }); describe("`restart` callback", () => { @@ -1597,7 +1841,9 @@ describe("ignoreResults", () => { await waitFor(() => { expect(onData).toHaveBeenCalledTimes(1); expect(onError).toHaveBeenCalledTimes(1); - expect(onError).toHaveBeenLastCalledWith(error); + expect(onError).toHaveBeenLastCalledWith( + new ApolloError({ protocolErrors: [error] }) + ); expect(onComplete).toHaveBeenCalledTimes(0); }); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index f3ef9aacb0e..8f56c095fe5 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -776,8 +776,8 @@ export function getDefaultFetchPolicy< ); } -function toApolloError( - result: ApolloQueryResult +export function toApolloError( + result: Pick, "errors" | "error"> ): ApolloError | undefined { return isNonEmptyArray(result.errors) ? new ApolloError({ graphQLErrors: result.errors }) diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts index d602578de73..0bbcb9cf17e 100644 --- a/src/react/hooks/useSubscription.ts +++ b/src/react/hooks/useSubscription.ts @@ -13,14 +13,16 @@ import type { import type { ApolloClient, DefaultContext, + ErrorPolicy, FetchPolicy, FetchResult, OperationVariables, } from "../../core/index.js"; -import { Observable } from "../../core/index.js"; +import { ApolloError, Observable } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; import { useDeepMemo } from "./internal/useDeepMemo.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; +import { toApolloError } from "./useQuery.js"; import { useIsomorphicLayoutEffect } from "./internal/useIsomorphicLayoutEffect.js"; /** @@ -138,18 +140,38 @@ export function useSubscription< } } - const { skip, fetchPolicy, shouldResubscribe, context, ignoreResults } = - options; + const { + skip, + fetchPolicy, + errorPolicy, + shouldResubscribe, + context, + ignoreResults, + } = options; const variables = useDeepMemo(() => options.variables, [options.variables]); let [observable, setObservable] = React.useState(() => options.skip ? null : ( - createSubscription(client, subscription, variables, fetchPolicy, context) + createSubscription( + client, + subscription, + variables, + fetchPolicy, + errorPolicy, + context + ) ) ); const recreate = () => - createSubscription(client, subscription, variables, fetchPolicy, context); + createSubscription( + client, + subscription, + variables, + fetchPolicy, + errorPolicy, + context + ); const recreateRef = React.useRef(recreate); useIsomorphicLayoutEffect(() => { @@ -165,6 +187,7 @@ export function useSubscription< ((client !== observable.__.client || subscription !== observable.__.query || fetchPolicy !== observable.__.fetchPolicy || + errorPolicy !== observable.__.errorPolicy || !equal(variables, observable.__.variables)) && (typeof shouldResubscribe === "function" ? !!shouldResubscribe(options!) @@ -223,13 +246,15 @@ export function useSubscription< // TODO: fetchResult.data can be null but SubscriptionResult.data // expects TData | undefined only data: fetchResult.data!, - error: void 0, + error: toApolloError(fetchResult), variables, }; observable.__.setResult(result); if (!ignoreResultsRef.current) update(); - if (optionsRef.current.onData) { + if (result.error) { + optionsRef.current.onError?.(result.error); + } else if (optionsRef.current.onData) { optionsRef.current.onData({ client, data: result, @@ -242,6 +267,10 @@ export function useSubscription< } }, error(error) { + error = + error instanceof ApolloError ? error : ( + new ApolloError({ protocolErrors: [error] }) + ); if (!subscriptionStopped) { observable.__.setResult({ loading: false, @@ -304,6 +333,7 @@ function createSubscription< query: TypedDocumentNode, variables?: TVariables, fetchPolicy?: FetchPolicy, + errorPolicy?: ErrorPolicy, context?: DefaultContext ) { const __ = { @@ -311,6 +341,7 @@ function createSubscription< client, query, fetchPolicy, + errorPolicy, result: { loading: true, data: void 0, @@ -332,6 +363,7 @@ function createSubscription< query, variables, fetchPolicy, + errorPolicy, context, }); } diff --git a/src/react/types/types.ts b/src/react/types/types.ts index be799bf52dd..ee441640737 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -437,6 +437,8 @@ export interface BaseSubscriptionOptions< variables?: TVariables; /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: FetchPolicy; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#errorPolicy:member} */ + errorPolicy?: ErrorPolicy; /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#shouldResubscribe:member} */ shouldResubscribe?: | boolean From 1b23337e5a9eec4ce3ed69531ca4f4afe8e897a6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 9 Jul 2024 08:16:30 -0600 Subject: [PATCH 45/62] Add ability to configure a name for the client for use with devtools (#11936) Co-authored-by: Lenz Weber-Tronic --- .api-reports/api-report-core.api.md | 12 ++++++ .api-reports/api-report-react.api.md | 12 ++++++ .../api-report-react_components.api.md | 12 ++++++ .api-reports/api-report-react_context.api.md | 12 ++++++ .api-reports/api-report-react_hoc.api.md | 12 ++++++ .api-reports/api-report-react_hooks.api.md | 12 ++++++ .api-reports/api-report-react_internal.api.md | 12 ++++++ .api-reports/api-report-react_ssr.api.md | 12 ++++++ .api-reports/api-report-testing.api.md | 12 ++++++ .api-reports/api-report-testing_core.api.md | 12 ++++++ .api-reports/api-report-utilities.api.md | 12 ++++++ .api-reports/api-report.api.md | 12 ++++++ .changeset/pink-ants-remember.md | 16 +++++++ .size-limits.json | 4 +- src/core/ApolloClient.ts | 43 +++++++++++++++++-- 15 files changed, 201 insertions(+), 6 deletions(-) create mode 100644 .changeset/pink-ants-remember.md diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 35789a58440..92b34976cf7 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -103,6 +103,10 @@ export class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; get documentTransform(): DocumentTransform; @@ -144,12 +148,14 @@ export class ApolloClient implements DataProxy { export interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -593,6 +599,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) export type DiffQueryAgainstStoreOptions = ReadQueryOptions & { returnPartialData?: boolean; diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 6fd83bb10f4..916035b05c5 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -114,6 +114,10 @@ class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts @@ -171,12 +175,14 @@ class ApolloClient implements DataProxy { interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -680,6 +686,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index 350691ae935..90af57d6d2e 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -114,6 +114,10 @@ class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts @@ -172,12 +176,14 @@ class ApolloClient implements DataProxy { interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -623,6 +629,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index 41aed0f90c4..f2ec9bb082b 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -113,6 +113,10 @@ class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts @@ -171,12 +175,14 @@ class ApolloClient implements DataProxy { interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -618,6 +624,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index 18f094dde4c..f82219194f5 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -113,6 +113,10 @@ class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts @@ -171,12 +175,14 @@ class ApolloClient implements DataProxy { interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -616,6 +622,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index 861d5c89198..094ea9d31a8 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -112,6 +112,10 @@ class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts @@ -170,12 +174,14 @@ class ApolloClient implements DataProxy { interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -646,6 +652,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 77a4d29c2f8..81bffbb7458 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -112,6 +112,10 @@ class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts @@ -170,12 +174,14 @@ class ApolloClient implements DataProxy { interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -631,6 +637,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" 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 ea788b305b9..ca7a9f6f209 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -113,6 +113,10 @@ class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts @@ -171,12 +175,14 @@ class ApolloClient implements DataProxy { interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -587,6 +593,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index 01ba05ec8b1..ffcefbd2d41 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -113,6 +113,10 @@ class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts @@ -171,12 +175,14 @@ class ApolloClient implements DataProxy { interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -588,6 +594,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" 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 545e30231cd..b0659710384 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -112,6 +112,10 @@ class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts @@ -170,12 +174,14 @@ class ApolloClient implements DataProxy { interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -587,6 +593,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" 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 84e1668f11b..7b26126fdc0 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -126,6 +126,10 @@ class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; get documentTransform(): DocumentTransform; @@ -183,12 +187,14 @@ class ApolloClient implements DataProxy { interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -779,6 +785,12 @@ const _deleteModifier: unique symbol; // @public @deprecated (undocumented) export const DEV: boolean; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) export type DirectiveInfo = { [fieldName: string]: { diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 181d29f8273..a56145500ed 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -105,6 +105,10 @@ export class ApolloClient implements DataProxy { get defaultContext(): Partial; // (undocumented) defaultOptions: DefaultOptions; + // Warning: (ae-forgotten-export) The symbol "DevtoolsOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly devtoolsConfig: DevtoolsOptions; // (undocumented) disableNetworkFetches: boolean; get documentTransform(): DocumentTransform; @@ -146,12 +150,14 @@ export class ApolloClient implements DataProxy { export interface ApolloClientOptions { assumeImmutableResults?: boolean; cache: ApolloCache; + // @deprecated connectToDevTools?: boolean; // (undocumented) credentials?: string; // (undocumented) defaultContext?: Partial; defaultOptions?: DefaultOptions; + devtools?: DevtoolsOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) @@ -694,6 +700,12 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +interface DevtoolsOptions { + enabled?: boolean; + name?: string; +} + // @public (undocumented) export type DiffQueryAgainstStoreOptions = ReadQueryOptions & { returnPartialData?: boolean; diff --git a/.changeset/pink-ants-remember.md b/.changeset/pink-ants-remember.md new file mode 100644 index 00000000000..b5d5506d5b7 --- /dev/null +++ b/.changeset/pink-ants-remember.md @@ -0,0 +1,16 @@ +--- +"@apollo/client": minor +--- + +Add the ability to specify a name for the client instance for use with Apollo Client Devtools. This is useful when instantiating multiple clients to identify the client instance more easily. This deprecates the `connectToDevtools` option in favor of a new `devtools` configuration. + +```ts +new ApolloClient({ + devtools: { + enabled: true, + name: "Test Client", + }, +}); +``` + +This option is backwards-compatible with `connectToDevtools` and will be used in the absense of a `devtools` option. diff --git a/.size-limits.json b/.size-limits.json index e7e76549d6e..c81dd92070b 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 40066, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32903 + "dist/apollo-client.min.cjs": 40110, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32941 } diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 05e713d520a..307ea715e85 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -41,6 +41,22 @@ export interface DefaultOptions { mutate?: Partial>; } +export interface DevtoolsOptions { + /** + * If `true`, the [Apollo Client Devtools](https://www.apollographql.com/docs/react/development-testing/developer-tooling/#apollo-client-devtools) browser extension can connect to this `ApolloClient` instance. + * + * The default value is `false` in production and `true` in development if there is a `window` object. + */ + enabled?: boolean; + + /** + * Optional name for this `ApolloClient` instance in the devtools. This is + * useful when you instantiate multiple clients and want to be able to + * identify them by name. + */ + name?: string; +} + let hasSuggestedDevtools = false; export interface ApolloClientOptions { @@ -85,6 +101,7 @@ export interface ApolloClientOptions { * If `true`, the [Apollo Client Devtools](https://www.apollographql.com/docs/react/development-testing/developer-tooling/#apollo-client-devtools) browser extension can connect to Apollo Client. * * The default value is `false` in production and `true` in development (if there is a `window` object). + * @deprecated Please use the `devtools.enabled` option. */ connectToDevTools?: boolean; /** @@ -120,6 +137,13 @@ export interface ApolloClientOptions { */ version?: string; documentTransform?: DocumentTransform; + + /** + * Configuration used by the [Apollo Client Devtools extension](https://www.apollographql.com/docs/react/development-testing/developer-tooling/#apollo-client-devtools) for this client. + * + * @since 3.11.0 + */ + devtools?: DevtoolsOptions; } // Though mergeOptions now resides in @apollo/client/utilities, it was @@ -148,6 +172,7 @@ export class ApolloClient implements DataProxy { public queryDeduplication: boolean; public defaultOptions: DefaultOptions; public readonly typeDefs: ApolloClientOptions["typeDefs"]; + public readonly devtoolsConfig: DevtoolsOptions; private queryManager: QueryManager; private devToolsHookCb?: Function; @@ -201,9 +226,7 @@ export class ApolloClient implements DataProxy { // Expose the client instance as window.__APOLLO_CLIENT__ and call // onBroadcast in queryManager.broadcastQueries to enable browser // devtools, but disable them by default in production. - connectToDevTools = typeof window === "object" && - !(window as any).__APOLLO_CLIENT__ && - __DEV__, + connectToDevTools, queryDeduplication = true, defaultOptions, defaultContext, @@ -213,6 +236,7 @@ export class ApolloClient implements DataProxy { fragmentMatcher, name: clientAwarenessName, version: clientAwarenessVersion, + devtools, } = options; let { link } = options; @@ -228,6 +252,17 @@ export class ApolloClient implements DataProxy { this.queryDeduplication = queryDeduplication; this.defaultOptions = defaultOptions || Object.create(null); this.typeDefs = typeDefs; + this.devtoolsConfig = { + ...devtools, + enabled: devtools?.enabled || connectToDevTools, + }; + + if (this.devtoolsConfig.enabled === undefined) { + this.devtoolsConfig.enabled = + typeof window === "object" && + (window as any).__APOLLO_CLIENT__ && + __DEV__; + } if (ssrForceFetchDelay) { setTimeout( @@ -283,7 +318,7 @@ export class ApolloClient implements DataProxy { : void 0, }); - if (connectToDevTools) this.connectToDevTools(); + if (this.devtoolsConfig.enabled) this.connectToDevTools(); } private connectToDevTools() { From 579330147d6bd6f7167a35413a33746103e375cb Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 9 Jul 2024 17:43:13 +0200 Subject: [PATCH 46/62] Change usages of the `GraphQLError` type to `GraphQLFormattedError`. (#11789) * post-process errors received from responses into GraphQLError instances * adjust types, add test * fixing more types * Clean up Prettier, Size-limit, and Api-Extractor * remove runtime components * add eslint rule * Clean up Prettier, Size-limit, and Api-Extractor * adjust some more * adjustments * change patch level --------- Co-authored-by: phryneas --- .api-reports/api-report-core.api.md | 35 +++++++++--------- .api-reports/api-report-errors.api.md | 24 +++++++----- .../api-report-link_batch-http.api.md | 13 ++++--- .api-reports/api-report-link_batch.api.md | 13 ++++--- .api-reports/api-report-link_context.api.md | 13 ++++--- .api-reports/api-report-link_core.api.md | 13 ++++--- .api-reports/api-report-link_error.api.md | 23 ++++++------ .api-reports/api-report-link_http.api.md | 13 ++++--- .../api-report-link_persisted-queries.api.md | 18 +++++---- .../api-report-link_remove-typename.api.md | 13 ++++--- .api-reports/api-report-link_retry.api.md | 13 ++++--- .api-reports/api-report-link_schema.api.md | 13 ++++--- .../api-report-link_subscriptions.api.md | 13 ++++--- .api-reports/api-report-link_ws.api.md | 13 ++++--- .api-reports/api-report-react.api.md | 35 +++++++++--------- .../api-report-react_components.api.md | 35 +++++++++--------- .api-reports/api-report-react_context.api.md | 35 +++++++++--------- .api-reports/api-report-react_hoc.api.md | 35 +++++++++--------- .api-reports/api-report-react_hooks.api.md | 35 +++++++++--------- .api-reports/api-report-react_internal.api.md | 35 +++++++++--------- .api-reports/api-report-react_ssr.api.md | 35 +++++++++--------- .api-reports/api-report-testing.api.md | 35 +++++++++--------- .api-reports/api-report-testing_core.api.md | 35 +++++++++--------- .api-reports/api-report-utilities.api.md | 37 +++++++++---------- .api-reports/api-report.api.md | 35 +++++++++--------- .changeset/slimy-berries-yawn.md | 14 +++++++ .eslintrc | 16 ++++++++ src/__tests__/client.ts | 6 +-- src/core/ApolloClient.ts | 6 ++- src/core/QueryInfo.ts | 4 +- src/core/types.ts | 4 +- src/errors/__tests__/ApolloError.ts | 17 ++++----- src/errors/index.ts | 30 +++++++++++---- src/link/core/types.ts | 10 +++-- src/link/error/index.ts | 8 ++-- src/link/persisted-queries/index.ts | 28 +++++++++----- src/link/subscriptions/index.ts | 6 ++- .../__tests__/client/Mutation.test.tsx | 8 +++- src/testing/experimental/createSchemaFetch.ts | 13 ++++++- 39 files changed, 442 insertions(+), 345 deletions(-) create mode 100644 .changeset/slimy-berries-yawn.md diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 92b34976cf7..815cb7391ad 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -9,12 +9,12 @@ import { disableExperimentalFragmentVariables } from 'graphql-tag'; import { disableFragmentWarnings } from 'graphql-tag'; import type { DocumentNode } from 'graphql'; import { enableExperimentalFragmentVariables } from 'graphql-tag'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; import { gql } from 'graphql-tag'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import type { InlineFragmentNode } from 'graphql'; import { InvariantError } from 'ts-invariant'; import { Observable } from 'zen-observable-ts'; @@ -94,7 +94,7 @@ export class ApolloClient implements DataProxy { __actionHookForDevTools(cb: () => any): void; constructor(options: ApolloClientOptions); // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; addResolvers(resolvers: Resolvers | Resolvers[]): void; // (undocumented) cache: ApolloCache; @@ -179,17 +179,15 @@ export class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -212,7 +210,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -266,7 +264,7 @@ export interface ApolloQueryResult { // (undocumented) data: T; error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // (undocumented) @@ -776,7 +774,7 @@ export interface ExecutionPatchInitialResult, TExten // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -993,9 +991,6 @@ const getInMemoryCacheMemoryInternals: (() => { export { gql } -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) export interface GraphQLRequest> { // (undocumented) @@ -1080,7 +1075,7 @@ export interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1712,7 +1707,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -2089,11 +2084,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -export interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +export interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-errors.api.md b/.api-reports/api-report-errors.api.md index 2b3d65a0558..96c8bafc999 100644 --- a/.api-reports/api-report-errors.api.md +++ b/.api-reports/api-report-errors.api.md @@ -4,23 +4,23 @@ ```ts -import type { ExecutionResult } from 'graphql'; import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; // @public (undocumented) export class ApolloError extends Error { constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -46,7 +46,7 @@ export interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -81,7 +81,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -113,7 +113,7 @@ type FetchResultWithSymbolExtensions = FetchResult & { extensions: Record; }; -// @public (undocumented) +// @public @deprecated (undocumented) export type GraphQLErrors = ReadonlyArray; // Warning: (ae-forgotten-export) The symbol "FetchResultWithSymbolExtensions" needs to be exported by the entry point index.d.ts @@ -126,7 +126,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -166,11 +166,15 @@ type ServerParseError = Error & { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-link_batch-http.api.md b/.api-reports/api-report-link_batch-http.api.md index 89008cc6985..f212ce55c05 100644 --- a/.api-reports/api-report-link_batch-http.api.md +++ b/.api-reports/api-report-link_batch-http.api.md @@ -6,8 +6,7 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -117,7 +116,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -179,7 +178,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -232,11 +231,15 @@ interface Printer { type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-link_batch.api.md b/.api-reports/api-report-link_batch.api.md index 6f6464edbbd..f7e4426ef61 100644 --- a/.api-reports/api-report-link_batch.api.md +++ b/.api-reports/api-report-link_batch.api.md @@ -5,8 +5,7 @@ ```ts import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -107,7 +106,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -153,7 +152,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -208,11 +207,15 @@ type Path = ReadonlyArray; type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-link_context.api.md b/.api-reports/api-report-link_context.api.md index 76a7c4e5344..cbf51621b32 100644 --- a/.api-reports/api-report-link_context.api.md +++ b/.api-reports/api-report-link_context.api.md @@ -5,8 +5,7 @@ ```ts import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -80,7 +79,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -124,7 +123,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -169,11 +168,15 @@ type RequestHandler = (operation: Operation, forward: NextLink) => Observable, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-link_core.api.md b/.api-reports/api-report-link_core.api.md index ce472253f3c..32f6e56f608 100644 --- a/.api-reports/api-report-link_core.api.md +++ b/.api-reports/api-report-link_core.api.md @@ -5,8 +5,7 @@ ```ts import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -83,7 +82,7 @@ export interface ExecutionPatchInitialResult, TExten // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -126,7 +125,7 @@ export interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -164,11 +163,15 @@ export type Path = ReadonlyArray; export type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; // @public (undocumented) -export interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +export interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-link_error.api.md b/.api-reports/api-report-link_error.api.md index 245cc7946c9..92cbae62eb5 100644 --- a/.api-reports/api-report-link_error.api.md +++ b/.api-reports/api-report-link_error.api.md @@ -5,8 +5,8 @@ ```ts import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -80,10 +80,8 @@ export class ErrorLink extends ApolloLink { export interface ErrorResponse { // (undocumented) forward: NextLink; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors?: GraphQLErrors; + graphQLErrors?: ReadonlyArray; // Warning: (ae-forgotten-export) The symbol "NetworkError" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -91,7 +89,7 @@ export interface ErrorResponse { // (undocumented) operation: Operation; // (undocumented) - response?: ExecutionResult; + response?: FormattedExecutionResult; } // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts @@ -115,7 +113,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -140,9 +138,6 @@ interface ExecutionPatchResultBase { // @public (undocumented) type FetchResult, TContext = Record, TExtensions = Record> = SingleExecutionResult | ExecutionPatchResult; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts @@ -164,7 +159,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -227,11 +222,15 @@ type ServerParseError = Error & { }; // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-link_http.api.md b/.api-reports/api-report-link_http.api.md index 6e74d1fd378..dd7cf6778c2 100644 --- a/.api-reports/api-report-link_http.api.md +++ b/.api-reports/api-report-link_http.api.md @@ -6,8 +6,7 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { InvariantError } from 'ts-invariant'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -116,7 +115,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -217,7 +216,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -311,11 +310,15 @@ export type ServerParseError = Error & { }; // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-link_persisted-queries.api.md b/.api-reports/api-report-link_persisted-queries.api.md index 7a977f4ce55..0e4fa0e4639 100644 --- a/.api-reports/api-report-link_persisted-queries.api.md +++ b/.api-reports/api-report-link_persisted-queries.api.md @@ -5,8 +5,8 @@ ```ts import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -88,7 +88,7 @@ type ErrorMeta = { // @public (undocumented) export interface ErrorResponse { // (undocumented) - graphQLErrors?: readonly GraphQLError[]; + graphQLErrors?: ReadonlyArray; // Warning: (ae-forgotten-export) The symbol "ErrorMeta" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -100,7 +100,7 @@ export interface ErrorResponse { // (undocumented) operation: Operation; // (undocumented) - response?: ExecutionResult; + response?: FormattedExecutionResult; } // Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts @@ -124,7 +124,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -173,7 +173,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -263,11 +263,15 @@ type ServerParseError = Error & { type SHA256Function = (...args: any[]) => string | PromiseLike; // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-link_remove-typename.api.md b/.api-reports/api-report-link_remove-typename.api.md index 05dcca3dac0..7d088615a2b 100644 --- a/.api-reports/api-report-link_remove-typename.api.md +++ b/.api-reports/api-report-link_remove-typename.api.md @@ -5,8 +5,7 @@ ```ts import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -75,7 +74,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -121,7 +120,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -191,11 +190,15 @@ export interface RemoveTypenameFromVariablesOptions { type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-link_retry.api.md b/.api-reports/api-report-link_retry.api.md index 173a281dd67..843ff5f1654 100644 --- a/.api-reports/api-report-link_retry.api.md +++ b/.api-reports/api-report-link_retry.api.md @@ -5,8 +5,7 @@ ```ts import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -88,7 +87,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -134,7 +133,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -208,11 +207,15 @@ export class RetryLink extends ApolloLink { } // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-link_schema.api.md b/.api-reports/api-report-link_schema.api.md index 14459f745e9..817398b7594 100644 --- a/.api-reports/api-report-link_schema.api.md +++ b/.api-reports/api-report-link_schema.api.md @@ -5,8 +5,7 @@ ```ts import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import type { GraphQLSchema } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -76,7 +75,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -122,7 +121,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -194,11 +193,15 @@ export class SchemaLink extends ApolloLink { } // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-link_subscriptions.api.md b/.api-reports/api-report-link_subscriptions.api.md index a67c5415721..b7fcb443f86 100644 --- a/.api-reports/api-report-link_subscriptions.api.md +++ b/.api-reports/api-report-link_subscriptions.api.md @@ -6,8 +6,7 @@ import type { Client } from 'graphql-ws'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; @@ -76,7 +75,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -133,7 +132,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -173,11 +172,15 @@ type Path = ReadonlyArray; type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-link_ws.api.md b/.api-reports/api-report-link_ws.api.md index 7969b0cdbc1..ec666d00c71 100644 --- a/.api-reports/api-report-link_ws.api.md +++ b/.api-reports/api-report-link_ws.api.md @@ -6,8 +6,7 @@ import type { ClientOptions } from 'subscriptions-transport-ws'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; -import type { GraphQLError } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import { SubscriptionClient } from 'subscriptions-transport-ws'; @@ -77,7 +76,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -123,7 +122,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -163,11 +162,15 @@ type Path = ReadonlyArray; type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 916035b05c5..32dd9f8010c 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -6,11 +6,11 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type * as ReactTypes from 'react'; @@ -102,7 +102,7 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts addResolvers(resolvers: Resolvers | Resolvers[]): void; // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts @@ -230,17 +230,15 @@ class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -266,7 +264,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -332,7 +330,7 @@ interface ApolloQueryResult { data: T; // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts @@ -760,7 +758,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -874,9 +872,6 @@ const getApolloClientMemoryInternals: (() => { // @public (undocumented) export function getApolloContext(): ReactTypes.Context; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // (undocumented) @@ -915,7 +910,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1480,7 +1475,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -1878,11 +1873,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -interface SingleExecutionResult, TContext = Context, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = Context, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index 90af57d6d2e..a1e25d69774 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -6,11 +6,11 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import * as PropTypes from 'prop-types'; @@ -102,7 +102,7 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts addResolvers(resolvers: Resolvers | Resolvers[]): void; // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts @@ -208,17 +208,15 @@ class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -244,7 +242,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -297,7 +295,7 @@ interface ApolloQueryResult { data: T; // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts @@ -692,7 +690,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -793,9 +791,6 @@ const getApolloClientMemoryInternals: (() => { }; }) | undefined; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // (undocumented) @@ -824,7 +819,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1300,7 +1295,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -1631,11 +1626,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index f2ec9bb082b..4c0c8a8991d 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -6,11 +6,11 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type * as ReactTypes from 'react'; @@ -101,7 +101,7 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts addResolvers(resolvers: Resolvers | Resolvers[]): void; // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts @@ -228,17 +228,15 @@ class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -264,7 +262,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -328,7 +326,7 @@ interface ApolloQueryResult { data: T; // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts @@ -687,7 +685,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -791,9 +789,6 @@ const getApolloClientMemoryInternals: (() => { // @public (undocumented) export function getApolloContext(): ReactTypes.Context; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // (undocumented) @@ -822,7 +817,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1229,7 +1224,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -1585,11 +1580,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index f82219194f5..ad7a5bf8b46 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -6,11 +6,11 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type * as ReactTypes from 'react'; @@ -101,7 +101,7 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts addResolvers(resolvers: Resolvers | Resolvers[]): void; // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts @@ -207,17 +207,15 @@ class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -243,7 +241,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -296,7 +294,7 @@ interface ApolloQueryResult { data: T; // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts @@ -685,7 +683,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -798,9 +796,6 @@ const getApolloClientMemoryInternals: (() => { // @public @deprecated (undocumented) export function graphql> & Partial>>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // (undocumented) @@ -829,7 +824,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1274,7 +1269,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -1591,11 +1586,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index 094ea9d31a8..2137e1da774 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -6,11 +6,11 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type { Subscriber } from 'zen-observable-ts'; @@ -100,7 +100,7 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts addResolvers(resolvers: Resolvers | Resolvers[]): void; // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts @@ -206,17 +206,15 @@ class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -242,7 +240,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -295,7 +293,7 @@ interface ApolloQueryResult { data: T; // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts @@ -715,7 +713,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -826,9 +824,6 @@ const getApolloClientMemoryInternals: (() => { }; }) | undefined; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // (undocumented) @@ -857,7 +852,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1355,7 +1350,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -1714,11 +1709,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 81bffbb7458..993f35c5615 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -6,11 +6,11 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type { Subscriber } from 'zen-observable-ts'; @@ -100,7 +100,7 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts addResolvers(resolvers: Resolvers | Resolvers[]): void; // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts @@ -206,17 +206,15 @@ class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -242,7 +240,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -295,7 +293,7 @@ interface ApolloQueryResult { data: T; // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts @@ -700,7 +698,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -834,9 +832,6 @@ export function getSuspenseCache(client: ApolloClient & { // @public (undocumented) export function getWrappedPromise(queryRef: WrappedQueryRef): QueryRefPromise; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // (undocumented) @@ -872,7 +867,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1401,7 +1396,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -1765,11 +1760,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index ca7a9f6f209..6db286b108c 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -6,11 +6,11 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type * as ReactTypes from 'react'; @@ -101,7 +101,7 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts addResolvers(resolvers: Resolvers | Resolvers[]): void; // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts @@ -207,17 +207,15 @@ class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -243,7 +241,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -296,7 +294,7 @@ interface ApolloQueryResult { data: T; // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts @@ -656,7 +654,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -776,9 +774,6 @@ type GetMarkupFromTreeOptions = { renderFunction?: (tree: ReactTypes.ReactElement) => string | PromiseLike; }; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // (undocumented) @@ -807,7 +802,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1214,7 +1209,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -1570,11 +1565,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index ffcefbd2d41..170a8acc433 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -6,11 +6,11 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import * as React_2 from 'react'; @@ -101,7 +101,7 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts addResolvers(resolvers: Resolvers | Resolvers[]): void; // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts @@ -207,17 +207,15 @@ class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -243,7 +241,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -296,7 +294,7 @@ interface ApolloQueryResult { data: T; // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts @@ -657,7 +655,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -758,9 +756,6 @@ const getApolloClientMemoryInternals: (() => { }; }) | undefined; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // (undocumented) @@ -789,7 +784,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1297,7 +1292,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -1617,11 +1612,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index b0659710384..aacf32eccc3 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -6,11 +6,11 @@ import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type { Subscriber } from 'zen-observable-ts'; @@ -100,7 +100,7 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts addResolvers(resolvers: Resolvers | Resolvers[]): void; // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts @@ -206,17 +206,15 @@ class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -242,7 +240,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -295,7 +293,7 @@ interface ApolloQueryResult { data: T; // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts @@ -656,7 +654,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -757,9 +755,6 @@ const getApolloClientMemoryInternals: (() => { }; }) | undefined; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // (undocumented) @@ -788,7 +783,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1252,7 +1247,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -1574,11 +1569,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index 7b26126fdc0..8dc5e2982b6 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -8,12 +8,12 @@ import type { ArgumentNode } from 'graphql'; import type { ASTNode } from 'graphql'; import type { DirectiveNode } from 'graphql'; import type { DocumentNode } from 'graphql'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; import type { FragmentSpreadNode } from 'graphql'; -import { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import { GraphQLFormattedError } from 'graphql'; import type { InlineFragmentNode } from 'graphql'; import type { NameNode } from 'graphql'; import { Observable } from 'zen-observable-ts'; @@ -114,7 +114,7 @@ class ApolloClient implements DataProxy { // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts // // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts addResolvers(resolvers: Resolvers | Resolvers[]): void; // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts @@ -219,17 +219,15 @@ class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -255,7 +253,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -319,7 +317,7 @@ interface ApolloQueryResult { data: T; // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts @@ -963,7 +961,7 @@ interface ExecutionPatchInitialResult, TExtensions = // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1169,7 +1167,7 @@ export function getFragmentQueryDocument(document: DocumentNode, fragmentName?: export type GetFragmentSpreadConfig = GetNodeConfig; // @public (undocumented) -export function getGraphQLErrorsFromResult(result: FetchResult): GraphQLError[]; +export function getGraphQLErrorsFromResult(result: FetchResult): GraphQLFormattedError[]; // @public (undocumented) export function getInclusionDirectives(directives: ReadonlyArray): InclusionDirectives; @@ -1220,9 +1218,6 @@ export const getStoreKeyName: ((fieldName: string, args?: Record | // @public (undocumented) export function getTypenameFromResult(result: Record, selectionSet: SelectionSetNode, fragmentMap?: FragmentMap): string | undefined; -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) interface GraphQLRequest> { // (undocumented) @@ -1287,7 +1282,7 @@ interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -2016,7 +2011,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -2418,11 +2413,15 @@ interface SharedWatchQueryOptions export function shouldInclude({ directives }: SelectionNode, variables?: Record): boolean; // @public (undocumented) -interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index a56145500ed..b8b6d180649 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -9,12 +9,12 @@ import { disableExperimentalFragmentVariables } from 'graphql-tag'; import { disableFragmentWarnings } from 'graphql-tag'; import type { DocumentNode } from 'graphql'; import { enableExperimentalFragmentVariables } from 'graphql-tag'; -import type { ExecutionResult } from 'graphql'; import type { FieldNode } from 'graphql'; +import type { FormattedExecutionResult } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; import { gql } from 'graphql-tag'; -import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; +import type { GraphQLFormattedError } from 'graphql'; import type { InlineFragmentNode } from 'graphql'; import { InvariantError } from 'ts-invariant'; import { Observable } from 'zen-observable-ts'; @@ -96,7 +96,7 @@ export class ApolloClient implements DataProxy { __actionHookForDevTools(cb: () => any): void; constructor(options: ApolloClientOptions); // (undocumented) - __requestRaw(payload: GraphQLRequest): Observable; + __requestRaw(payload: GraphQLRequest): Observable; addResolvers(resolvers: Resolvers | Resolvers[]): void; // (undocumented) cache: ApolloCache; @@ -202,17 +202,15 @@ export class ApolloError extends Error { // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); cause: ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) | null; + readonly message: string; + extensions?: GraphQLErrorExtensions[] | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // (undocumented) clientErrors: ReadonlyArray; // (undocumented) extraInfo: any; - // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts - // // (undocumented) - graphQLErrors: GraphQLErrors; + graphQLErrors: ReadonlyArray; // (undocumented) message: string; // (undocumented) @@ -235,7 +233,7 @@ interface ApolloErrorOptions { // (undocumented) extraInfo?: any; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) networkError?: Error | ServerParseError | ServerError | null; // (undocumented) @@ -302,7 +300,7 @@ export interface ApolloQueryResult { // (undocumented) data: T; error?: ApolloError; - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) loading: boolean; // (undocumented) @@ -888,7 +886,7 @@ export interface ExecutionPatchInitialResult, TExten // (undocumented) data: TData | null | undefined; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -1116,9 +1114,6 @@ const getInMemoryCacheMemoryInternals: (() => { export { gql } -// @public (undocumented) -type GraphQLErrors = ReadonlyArray; - // @public (undocumented) export interface GraphQLRequest> { // (undocumented) @@ -1213,7 +1208,7 @@ export interface IncrementalPayload { // (undocumented) data: TData | null; // (undocumented) - errors?: ReadonlyArray; + errors?: ReadonlyArray; // (undocumented) extensions?: TExtensions; // (undocumented) @@ -2056,7 +2051,7 @@ class QueryInfo { // (undocumented) getDiff(): Cache_2.DiffResult; // (undocumented) - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; // (undocumented) init(query: { document: DocumentNode; @@ -2498,11 +2493,15 @@ interface SharedWatchQueryOptions } // @public (undocumented) -export interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { +export interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> { // (undocumented) context?: TContext; // (undocumented) data?: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; } // @public (undocumented) diff --git a/.changeset/slimy-berries-yawn.md b/.changeset/slimy-berries-yawn.md new file mode 100644 index 00000000000..ef7d7ec671c --- /dev/null +++ b/.changeset/slimy-berries-yawn.md @@ -0,0 +1,14 @@ +--- +"@apollo/client": minor +--- + +Changes usages of the `GraphQLError` type to `GraphQLFormattedError`. + +This was a type bug - these errors were never `GraphQLError` instances +to begin with, and the `GraphQLError` class has additional properties that can +never be correctly rehydrated from a GraphQL result. +The correct type to use here is `GraphQLFormattedError`. + +Similarly, please ensure to use the type `FormattedExecutionResult` +instead of `ExecutionResult` - the non-"Formatted" versions of these types +are for use on the server only, but don't get transported over the network. diff --git a/.eslintrc b/.eslintrc index b4d6d6f5363..e8abca31af1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -40,6 +40,22 @@ ], "@typescript-eslint/consistent-type-exports": ["error"], "@typescript-eslint/no-import-type-side-effects": "error", + "@typescript-eslint/ban-types": [ + "error", + { + "types": { + "GraphQLError": { + "message": "Use GraphQLFormattedError instead", + "fixWith": "GraphQLFormattedError" + }, + "ExecutionResult": { + "message": "Use FormattedExecutionResult instead", + "fixWith": "FormattedExecutionResult" + } + }, + "extendDefaults": false + } + ], "no-restricted-syntax": [ "error", { diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index cf8df19c268..c16b01d593e 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -1,11 +1,11 @@ import { cloneDeep, assign } from "lodash"; import { GraphQLError, - ExecutionResult, DocumentNode, Kind, print, visit, + FormattedExecutionResult, } from "graphql"; import gql from "graphql-tag"; @@ -752,7 +752,7 @@ describe("client", () => { cache: new InMemoryCache({ addTypename: false }), }); - return client.query({ query }).then((result: ExecutionResult) => { + return client.query({ query }).then((result: FormattedExecutionResult) => { expect(result.data).toEqual(data); }); }); @@ -6411,7 +6411,7 @@ function clientRoundtrip( resolve: (result: any) => any, reject: (reason: any) => any, query: DocumentNode, - data: ExecutionResult, + data: FormattedExecutionResult, variables?: any, possibleTypes?: PossibleTypesMap ) { diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 307ea715e85..39fc6f3d503 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -1,6 +1,6 @@ import { invariant, newInvariantError } from "../utilities/globals/index.js"; -import type { ExecutionResult, DocumentNode } from "graphql"; +import type { DocumentNode, FormattedExecutionResult } from "graphql"; import type { FetchResult, GraphQLRequest } from "../link/core/index.js"; import { ApolloLink, execute } from "../link/core/index.js"; @@ -606,7 +606,9 @@ export class ApolloClient implements DataProxy { this.devToolsHookCb = cb; } - public __requestRaw(payload: GraphQLRequest): Observable { + public __requestRaw( + payload: GraphQLRequest + ): Observable { return execute(this.link, payload); } diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index 2e8c149eedd..d4058bae2af 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -1,4 +1,4 @@ -import type { DocumentNode, GraphQLError } from "graphql"; +import type { DocumentNode, GraphQLFormattedError } from "graphql"; import { equal } from "@wry/equality"; import type { Cache, ApolloCache } from "../cache/index.js"; @@ -82,7 +82,7 @@ export class QueryInfo { variables?: Record; networkStatus?: NetworkStatus; networkError?: Error | null; - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; stopped = false; private cache: ApolloCache; diff --git a/src/core/types.ts b/src/core/types.ts index 8085d013839..fefe245b04c 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,4 +1,4 @@ -import type { DocumentNode, GraphQLError } from "graphql"; +import type { DocumentNode, GraphQLFormattedError } from "graphql"; import type { ApolloCache } from "../cache/index.js"; import type { FetchResult } from "../link/core/index.js"; @@ -145,7 +145,7 @@ export interface ApolloQueryResult { * A list of any errors that occurred during server-side execution of a GraphQL operation. * See https://www.apollographql.com/docs/react/data/error-handling/ for more information. */ - errors?: ReadonlyArray; + errors?: ReadonlyArray; /** * The single Error object that is passed to onError and useQuery hooks, and is often thrown during manual `client.query` calls. * This will contain both a NetworkError field and any GraphQLErrors. diff --git a/src/errors/__tests__/ApolloError.ts b/src/errors/__tests__/ApolloError.ts index 1881bade231..e672536f667 100644 --- a/src/errors/__tests__/ApolloError.ts +++ b/src/errors/__tests__/ApolloError.ts @@ -1,11 +1,10 @@ import { ApolloError } from ".."; -import { GraphQLError } from "graphql"; describe("ApolloError", () => { it("should construct itself correctly", () => { const graphQLErrors = [ - new GraphQLError("Something went wrong with GraphQL"), - new GraphQLError("Something else went wrong with GraphQL"), + { message: "Something went wrong with GraphQL" }, + { message: "Something else went wrong with GraphQL" }, ]; const protocolErrors = [ { @@ -41,7 +40,7 @@ describe("ApolloError", () => { }); it("should add a graphql error to the message", () => { - const graphQLErrors = [new GraphQLError("this is an error message")]; + const graphQLErrors = [{ message: "this is an error message" }]; const apolloError = new ApolloError({ graphQLErrors, }); @@ -51,8 +50,8 @@ describe("ApolloError", () => { it("should add multiple graphql errors to the message", () => { const graphQLErrors = [ - new GraphQLError("this is new"), - new GraphQLError("this is old"), + { message: "this is new" }, + { message: "this is old" }, ]; const apolloError = new ApolloError({ graphQLErrors, @@ -64,7 +63,7 @@ describe("ApolloError", () => { }); it("should add both network and graphql errors to the message", () => { - const graphQLErrors = [new GraphQLError("graphql error message")]; + const graphQLErrors = [{ message: "graphql error message" }]; const networkError = new Error("network error message"); const apolloError = new ApolloError({ graphQLErrors, @@ -77,7 +76,7 @@ describe("ApolloError", () => { }); it("should add both protocol and graphql errors to the message", () => { - const graphQLErrors = [new GraphQLError("graphql error message")]; + const graphQLErrors = [{ message: "graphql error message" }]; const protocolErrors = [ { message: "cannot read message from websocket", @@ -99,7 +98,7 @@ describe("ApolloError", () => { }); it("should contain a stack trace", () => { - const graphQLErrors = [new GraphQLError("graphql error message")]; + const graphQLErrors = [{ message: "graphql error message" }]; const networkError = new Error("network error message"); const apolloError = new ApolloError({ graphQLErrors, diff --git a/src/errors/index.ts b/src/errors/index.ts index 3c07411161b..11a14007734 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -1,6 +1,10 @@ import "../utilities/globals/index.js"; -import type { GraphQLError, GraphQLErrorExtensions } from "graphql"; +import type { + GraphQLError, + GraphQLErrorExtensions, + GraphQLFormattedError, +} from "graphql"; import { isNonNullObject } from "../utilities/index.js"; import type { ServerParseError } from "../link/http/index.js"; @@ -17,7 +21,7 @@ type FetchResultWithSymbolExtensions = FetchResult & { }; export interface ApolloErrorOptions { - graphQLErrors?: ReadonlyArray; + graphQLErrors?: ReadonlyArray; protocolErrors?: ReadonlyArray<{ message: string; extensions?: GraphQLErrorExtensions[]; @@ -67,6 +71,13 @@ const generateErrorMessage = (err: ApolloError) => { ); }; +/** + * @deprecated This type is deprecated and will be removed in the next major version of Apollo Client. + * It mistakenly referenced `GraqhQLError` instead of `GraphQLFormattedError`. + * + * Use `ReadonlyArray` instead. + */ +// eslint-disable-next-line @typescript-eslint/ban-types export type GraphQLErrors = ReadonlyArray; export type NetworkError = Error | ServerParseError | ServerError | null; @@ -74,7 +85,7 @@ export type NetworkError = Error | ServerParseError | ServerError | null; export class ApolloError extends Error { public name: string; public message: string; - public graphQLErrors: GraphQLErrors; + public graphQLErrors: ReadonlyArray; public protocolErrors: ReadonlyArray<{ message: string; extensions?: GraphQLErrorExtensions[]; @@ -88,9 +99,11 @@ export class ApolloError extends Error { */ public cause: | ({ - message: string; - extensions?: GraphQLErrorExtensions[]; - } & Partial) + readonly message: string; + extensions?: + | GraphQLErrorExtensions[] + | GraphQLFormattedError["extensions"]; + } & Omit & Partial, "extensions">) | null; // An object that can be used to provide some additional information @@ -98,8 +111,9 @@ export class ApolloError extends Error { // internally within Apollo Client. public extraInfo: any; - // Constructs an instance of ApolloError given a GraphQLError - // or a network error. Note that one of these has to be a valid + // Constructs an instance of ApolloError given serialized GraphQL errors, + // client errors, protocol errors or network errors. + // Note that one of these has to be a valid // value or the constructed error will be meaningless. constructor({ graphQLErrors, diff --git a/src/link/core/types.ts b/src/link/core/types.ts index a898f1a598e..c596ecac0c2 100644 --- a/src/link/core/types.ts +++ b/src/link/core/types.ts @@ -1,4 +1,4 @@ -import type { ExecutionResult, GraphQLError } from "graphql"; +import type { GraphQLFormattedError } from "graphql"; import type { DocumentNode } from "graphql"; import type { DefaultContext } from "../../core/index.js"; export type { DocumentNode }; @@ -18,7 +18,7 @@ export interface ExecutionPatchInitialResult< // if data is present, incremental is not data: TData | null | undefined; incremental?: never; - errors?: ReadonlyArray; + errors?: ReadonlyArray; extensions?: TExtensions; } @@ -28,7 +28,7 @@ export interface IncrementalPayload { data: TData | null; label?: string; path: Path; - errors?: ReadonlyArray; + errors?: ReadonlyArray; extensions?: TExtensions; } @@ -91,10 +91,12 @@ export interface SingleExecutionResult< TData = Record, TContext = DefaultContext, TExtensions = Record, -> extends ExecutionResult { +> { // data might be undefined if errorPolicy was set to 'ignore' data?: TData | null; context?: TContext; + errors?: ReadonlyArray; + extensions?: TExtensions; } export type FetchResult< diff --git a/src/link/error/index.ts b/src/link/error/index.ts index 00b0701ab6f..bf9494c5dfa 100644 --- a/src/link/error/index.ts +++ b/src/link/error/index.ts @@ -1,14 +1,14 @@ -import type { ExecutionResult } from "graphql"; +import type { FormattedExecutionResult, GraphQLFormattedError } from "graphql"; -import type { NetworkError, GraphQLErrors } from "../../errors/index.js"; +import type { NetworkError } from "../../errors/index.js"; import { Observable } from "../../utilities/index.js"; import type { Operation, FetchResult, NextLink } from "../core/index.js"; import { ApolloLink } from "../core/index.js"; export interface ErrorResponse { - graphQLErrors?: GraphQLErrors; + graphQLErrors?: ReadonlyArray; networkError?: NetworkError; - response?: ExecutionResult; + response?: FormattedExecutionResult; operation: Operation; forward: NextLink; } diff --git a/src/link/persisted-queries/index.ts b/src/link/persisted-queries/index.ts index 920633bd7f3..e631098652d 100644 --- a/src/link/persisted-queries/index.ts +++ b/src/link/persisted-queries/index.ts @@ -1,7 +1,11 @@ import { invariant } from "../../utilities/globals/index.js"; import { print } from "../../utilities/index.js"; -import type { DocumentNode, ExecutionResult, GraphQLError } from "graphql"; +import type { + DocumentNode, + FormattedExecutionResult, + GraphQLFormattedError, +} from "graphql"; import type { Operation } from "../core/index.js"; import { ApolloLink } from "../core/index.js"; @@ -21,9 +25,9 @@ import { export const VERSION = 1; export interface ErrorResponse { - graphQLErrors?: readonly GraphQLError[]; + graphQLErrors?: ReadonlyArray; networkError?: NetworkError; - response?: ExecutionResult; + response?: FormattedExecutionResult; operation: Operation; meta: ErrorMeta; } @@ -59,7 +63,10 @@ export namespace PersistedQueryLink { } function processErrors( - graphQLErrors: GraphQLError[] | readonly GraphQLError[] | undefined + graphQLErrors: + | GraphQLFormattedError[] + | ReadonlyArray + | undefined ): ErrorMeta { const byMessage = Object.create(null), byCode = Object.create(null); @@ -165,7 +172,7 @@ export const createPersistedQueryLink = ( const { query } = operation; - return new Observable((observer: Observer) => { + return new Observable((observer: Observer) => { let subscription: ObservableSubscription; let retried = false; let originalFetchOptions: any; @@ -174,13 +181,16 @@ export const createPersistedQueryLink = ( { response, networkError, - }: { response?: ExecutionResult; networkError?: ServerError }, + }: { + response?: FormattedExecutionResult; + networkError?: ServerError; + }, cb: () => void ) => { if (!retried && ((response && response.errors) || networkError)) { retried = true; - const graphQLErrors: GraphQLError[] = []; + const graphQLErrors: GraphQLFormattedError[] = []; const responseErrors = response && response.errors; if (isNonEmptyArray(responseErrors)) { @@ -193,7 +203,7 @@ export const createPersistedQueryLink = ( networkErrors = networkError && networkError.result && - (networkError.result.errors as GraphQLError[]); + (networkError.result.errors as GraphQLFormattedError[]); } if (isNonEmptyArray(networkErrors)) { graphQLErrors.push(...networkErrors); @@ -243,7 +253,7 @@ export const createPersistedQueryLink = ( cb(); }; const handler = { - next: (response: ExecutionResult) => { + next: (response: FormattedExecutionResult) => { maybeRetry({ response }, () => observer.next!(response)); }, error: (networkError: ServerError) => { diff --git a/src/link/subscriptions/index.ts b/src/link/subscriptions/index.ts index db56154718c..4823f7a22ee 100644 --- a/src/link/subscriptions/index.ts +++ b/src/link/subscriptions/index.ts @@ -29,12 +29,13 @@ // THE SOFTWARE. import { print } from "../../utilities/index.js"; -import type { Client } from "graphql-ws"; +import type { Client, Sink } from "graphql-ws"; import type { Operation, FetchResult } from "../core/index.js"; import { ApolloLink } from "../core/index.js"; import { isNonNullObject, Observable } from "../../utilities/index.js"; import { ApolloError } from "../../errors/index.js"; +import type { FormattedExecutionResult } from "graphql"; // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close_event function isLikeCloseEvent(val: unknown): val is CloseEvent { @@ -80,7 +81,8 @@ export class GraphQLWsLink extends ApolloLink { }) ); }, - } + // casting around a wrong type in graphql-ws, which incorrectly expects `Sink` + } satisfies Sink as any ); }); } diff --git a/src/react/components/__tests__/client/Mutation.test.tsx b/src/react/components/__tests__/client/Mutation.test.tsx index b71ba82f7cc..fc88f76b44e 100644 --- a/src/react/components/__tests__/client/Mutation.test.tsx +++ b/src/react/components/__tests__/client/Mutation.test.tsx @@ -1,6 +1,10 @@ import React, { useState, PropsWithChildren } from "react"; import gql from "graphql-tag"; -import { ExecutionResult, GraphQLError } from "graphql"; +import { + ExecutionResult, + FormattedExecutionResult, + GraphQLError, +} from "graphql"; import userEvent from "@testing-library/user-event"; import { render, screen, waitFor, act } from "@testing-library/react"; @@ -1195,7 +1199,7 @@ describe("General Mutation testing", () => { })); it("has an update prop for updating the store after the mutation", async () => { - const update = (_proxy: DataProxy, response: ExecutionResult) => { + const update = (_proxy: DataProxy, response: FormattedExecutionResult) => { expect(response.data).toEqual(data); }; diff --git a/src/testing/experimental/createSchemaFetch.ts b/src/testing/experimental/createSchemaFetch.ts index 5c03ea0da0d..fe253f98899 100644 --- a/src/testing/experimental/createSchemaFetch.ts +++ b/src/testing/experimental/createSchemaFetch.ts @@ -1,5 +1,5 @@ import { execute, validate } from "graphql"; -import type { GraphQLError, GraphQLSchema } from "graphql"; +import type { GraphQLFormattedError, GraphQLSchema } from "graphql"; import { ApolloError, gql } from "../../core/index.js"; import { withCleanup } from "../internal/index.js"; import { wait } from "../core/wait.js"; @@ -67,7 +67,16 @@ const createSchemaFetch = ( validationErrors = validate(schema, document); } catch (e) { validationErrors = [ - new ApolloError({ graphQLErrors: [e as GraphQLError] }), + new ApolloError({ + graphQLErrors: [ + /* + * Technically, these are even `GraphQLError` instances, + * but we try to avoid referencing that type, and `GraphQLError` + * implements the `GraphQLFormattedError` interface. + */ + e as GraphQLFormattedError, + ], + }), ]; } From 3812800c6e4e5e3e64f473543babdba35ce100c2 Mon Sep 17 00:00:00 2001 From: jcostello-atlassian <64562665+jcostello-atlassian@users.noreply.github.com> Date: Tue, 9 Jul 2024 12:01:50 -0400 Subject: [PATCH 47/62] Support extensions in useSubscription (#11854) Co-authored-by: Lenz Weber-Tronic --- .api-reports/api-report-core.api.md | 9 +-- .api-reports/api-report-react.api.md | 10 +-- .../api-report-react_components.api.md | 10 +-- .api-reports/api-report-react_context.api.md | 9 +-- .api-reports/api-report-react_hoc.api.md | 9 +-- .api-reports/api-report-react_hooks.api.md | 10 +-- .api-reports/api-report-react_internal.api.md | 9 +-- .api-reports/api-report-react_ssr.api.md | 9 +-- .api-reports/api-report-testing.api.md | 9 +-- .api-reports/api-report-testing_core.api.md | 9 +-- .api-reports/api-report-utilities.api.md | 9 +-- .api-reports/api-report.api.md | 10 +-- .changeset/angry-seals-jog.md | 5 ++ .size-limits.json | 4 +- src/core/QueryManager.ts | 72 ++++++++++--------- src/core/watchQueryOptions.ts | 3 + .../hooks/__tests__/useSubscription.test.tsx | 68 ++++++++++++++++++ src/react/hooks/useSubscription.ts | 49 ++++++------- src/react/types/types.documentation.ts | 5 ++ src/react/types/types.ts | 2 + 20 files changed, 209 insertions(+), 111 deletions(-) create mode 100644 .changeset/angry-seals-jog.md diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 815cb7391ad..5382952aee8 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1866,7 +1866,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -2142,6 +2142,7 @@ export type SubscribeToMoreOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -2302,9 +2303,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:390:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272: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 32dd9f8010c..0e32289572a 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -392,6 +392,7 @@ export interface BaseSubscriptionOptions; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts fetchPolicy?: FetchPolicy; ignoreResults?: boolean; @@ -1647,7 +1648,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1948,6 +1949,7 @@ export interface SubscriptionHookOptions { context?: Context; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -2324,11 +2326,11 @@ interface WatchQueryOptions; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts fetchPolicy?: FetchPolicy; ignoreResults?: boolean; @@ -1461,7 +1462,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1696,6 +1697,7 @@ export interface SubscriptionComponentOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1803,11 +1805,11 @@ interface WatchQueryOptions { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1626,6 +1626,7 @@ type SubscribeToMoreOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1724,11 +1725,11 @@ interface WatchQueryOptions { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1632,6 +1632,7 @@ type SubscribeToMoreOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1753,11 +1754,11 @@ export function withSubscription; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts fetchPolicy?: FetchPolicy; ignoreResults?: boolean; @@ -1516,7 +1517,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1770,6 +1771,7 @@ interface SubscriptionHookOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -2148,11 +2150,11 @@ interface WatchQueryOptions { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1815,6 +1815,7 @@ type SubscribeToMoreOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -2212,11 +2213,11 @@ export function wrapQueryRef(inter // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:390:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272: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 diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index 6db286b108c..c547e869238 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1375,7 +1375,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1611,6 +1611,7 @@ type SubscribeToMoreOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1709,11 +1710,11 @@ interface WatchQueryOptions { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1661,6 +1661,7 @@ type SubscribeToMoreOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1779,11 +1780,11 @@ export function withWarningSpy(it: (...args: TArgs // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:390:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index aacf32eccc3..ddf2c0aa6c4 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1413,7 +1413,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -1618,6 +1618,7 @@ type SubscribeToMoreOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -1736,11 +1737,11 @@ export function withWarningSpy(it: (...args: TArgs // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:390:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index 8dc5e2982b6..ea225f4f7e6 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -2177,7 +2177,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -2478,6 +2478,7 @@ type SubscribeToMoreOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -2667,11 +2668,11 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:390:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/utilities/graphql/storeUtils.ts:226:12 - (ae-forgotten-export) The symbol "storeKeyNameStringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index b8b6d180649..58d5f5cbdd7 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -363,6 +363,7 @@ export interface BaseSubscriptionOptions; context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; ignoreResults?: boolean; onComplete?: () => void; @@ -2216,7 +2217,7 @@ class QueryManager { // (undocumented) readonly ssrMode: boolean; // (undocumented) - startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, extensions, }: SubscriptionOptions): Observable>; stop(): void; // (undocumented) stopQuery(queryId: string): void; @@ -2580,6 +2581,7 @@ export interface SubscriptionHookOptions { context?: DefaultContext; errorPolicy?: ErrorPolicy; + extensions?: Record; fetchPolicy?: FetchPolicy; query: DocumentNode | TypedDocumentNode; variables?: TVariables; @@ -3014,9 +3016,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:390:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:272: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 diff --git a/.changeset/angry-seals-jog.md b/.changeset/angry-seals-jog.md new file mode 100644 index 00000000000..9ba52ae2f3c --- /dev/null +++ b/.changeset/angry-seals-jog.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Support extensions in useSubscription diff --git a/.size-limits.json b/.size-limits.json index c81dd92070b..3e400e47793 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 40110, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32941 + "dist/apollo-client.min.cjs": 40179, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32973 } diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 92029d9a6f1..9d230fcc3c0 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -293,6 +293,7 @@ export class QueryManager { optimisticResponse: isOptimistic ? optimisticResponse : void 0, }, variables, + {}, false ), @@ -981,52 +982,55 @@ export class QueryManager { errorPolicy = "none", variables, context = {}, + extensions = {}, }: SubscriptionOptions): Observable> { query = this.transform(query); variables = this.getVariables(query, variables); const makeObservable = (variables: OperationVariables) => - this.getObservableFromLink(query, context, variables).map((result) => { - if (fetchPolicy !== "no-cache") { - // the subscription interface should handle not sending us results we no longer subscribe to. - // XXX I don't think we ever send in an object with errors, but we might in the future... - if (shouldWriteResult(result, errorPolicy)) { - this.cache.write({ - query, - result: result.data, - dataId: "ROOT_SUBSCRIPTION", - variables: variables, - }); + this.getObservableFromLink(query, context, variables, extensions).map( + (result) => { + if (fetchPolicy !== "no-cache") { + // the subscription interface should handle not sending us results we no longer subscribe to. + // XXX I don't think we ever send in an object with errors, but we might in the future... + if (shouldWriteResult(result, errorPolicy)) { + this.cache.write({ + query, + result: result.data, + dataId: "ROOT_SUBSCRIPTION", + variables: variables, + }); + } + + this.broadcastQueries(); } - this.broadcastQueries(); - } + const hasErrors = graphQLResultHasError(result); + const hasProtocolErrors = graphQLResultHasProtocolErrors(result); + if (hasErrors || hasProtocolErrors) { + const errors: ApolloErrorOptions = {}; + if (hasErrors) { + errors.graphQLErrors = result.errors; + } + if (hasProtocolErrors) { + errors.protocolErrors = result.extensions[PROTOCOL_ERRORS_SYMBOL]; + } - const hasErrors = graphQLResultHasError(result); - const hasProtocolErrors = graphQLResultHasProtocolErrors(result); - if (hasErrors || hasProtocolErrors) { - const errors: ApolloErrorOptions = {}; - if (hasErrors) { - errors.graphQLErrors = result.errors; - } - if (hasProtocolErrors) { - errors.protocolErrors = result.extensions[PROTOCOL_ERRORS_SYMBOL]; + // `errorPolicy` is a mechanism for handling GraphQL errors, according + // to our documentation, so we throw protocol errors regardless of the + // set error policy. + if (errorPolicy === "none" || hasProtocolErrors) { + throw new ApolloError(errors); + } } - // `errorPolicy` is a mechanism for handling GraphQL errors, according - // to our documentation, so we throw protocol errors regardless of the - // set error policy. - if (errorPolicy === "none" || hasProtocolErrors) { - throw new ApolloError(errors); + if (errorPolicy === "ignore") { + delete result.errors; } - } - if (errorPolicy === "ignore") { - delete result.errors; + return result; } - - return result; - }); + ); if (this.getDocumentInfo(query).hasClientExports) { const observablePromise = this.localState @@ -1088,6 +1092,7 @@ export class QueryManager { query: DocumentNode, context: any, variables?: OperationVariables, + extensions?: Record, // Prefer context.queryDeduplication if specified. deduplication: boolean = context?.queryDeduplication ?? this.queryDeduplication @@ -1106,6 +1111,7 @@ export class QueryManager { ...context, forceFetch: !deduplication, }), + extensions, }; context = operation.context; diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index 5810c6464c4..b05cdb13c33 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -206,6 +206,9 @@ export interface SubscriptionOptions< /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#context:member} */ context?: DefaultContext; + + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#extensions:member} */ + extensions?: Record; } export interface MutationBaseOptions< diff --git a/src/react/hooks/__tests__/useSubscription.test.tsx b/src/react/hooks/__tests__/useSubscription.test.tsx index c003585f309..0c9002638d1 100644 --- a/src/react/hooks/__tests__/useSubscription.test.tsx +++ b/src/react/hooks/__tests__/useSubscription.test.tsx @@ -459,6 +459,74 @@ describe("useSubscription Hook", () => { expect(context!).toBe("Audi"); }); + it("should share extensions set in options", async () => { + const subscription = gql` + subscription { + car { + make + } + } + `; + + const results = ["Audi", "BMW"].map((make) => ({ + result: { data: { car: { make } } }, + })); + + let extensions: string; + const link = new MockSubscriptionLink(); + const extensionsLink = new ApolloLink((operation, forward) => { + extensions = operation.extensions.make; + return forward(operation); + }); + const client = new ApolloClient({ + link: concat(extensionsLink, link), + cache: new Cache({ addTypename: false }), + }); + + const { result } = renderHook( + () => + useSubscription(subscription, { + extensions: { make: "Audi" }, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + expect(result.current.loading).toBe(true); + expect(result.current.error).toBe(undefined); + expect(result.current.data).toBe(undefined); + setTimeout(() => { + link.simulateResult(results[0]); + }, 100); + + await waitFor( + () => { + expect(result.current.data).toEqual(results[0].result.data); + }, + { interval: 1 } + ); + expect(result.current.loading).toBe(false); + expect(result.current.error).toBe(undefined); + + setTimeout(() => { + link.simulateResult(results[1]); + }); + + await waitFor( + () => { + expect(result.current.data).toEqual(results[1].result.data); + }, + { interval: 1 } + ); + expect(result.current.loading).toBe(false); + expect(result.current.error).toBe(undefined); + + expect(extensions!).toBe("Audi"); + }); + it("should handle multiple subscriptions properly", async () => { const subscription = gql` subscription { diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts index 0bbcb9cf17e..fc2280c7bfb 100644 --- a/src/react/hooks/useSubscription.ts +++ b/src/react/hooks/useSubscription.ts @@ -146,23 +146,11 @@ export function useSubscription< errorPolicy, shouldResubscribe, context, + extensions, ignoreResults, } = options; const variables = useDeepMemo(() => options.variables, [options.variables]); - let [observable, setObservable] = React.useState(() => - options.skip ? null : ( - createSubscription( - client, - subscription, - variables, - fetchPolicy, - errorPolicy, - context - ) - ) - ); - const recreate = () => createSubscription( client, @@ -170,9 +158,14 @@ export function useSubscription< variables, fetchPolicy, errorPolicy, - context + context, + extensions ); + let [observable, setObservable] = React.useState( + options.skip ? null : recreate + ); + const recreateRef = React.useRef(recreate); useIsomorphicLayoutEffect(() => { recreateRef.current = recreate; @@ -331,17 +324,23 @@ function createSubscription< >( client: ApolloClient, query: TypedDocumentNode, - variables?: TVariables, - fetchPolicy?: FetchPolicy, - errorPolicy?: ErrorPolicy, - context?: DefaultContext + variables: TVariables | undefined, + fetchPolicy: FetchPolicy | undefined, + errorPolicy: ErrorPolicy | undefined, + context: DefaultContext | undefined, + extensions: Record | undefined ) { - const __ = { - variables, - client, + const options = { query, + variables, fetchPolicy, errorPolicy, + context, + extensions, + }; + const __ = { + ...options, + client, result: { loading: true, data: void 0, @@ -359,13 +358,7 @@ function createSubscription< // lazily start the subscription when the first observer subscribes // to get around strict mode if (!observable) { - observable = client.subscribe({ - query, - variables, - fetchPolicy, - errorPolicy, - context, - }); + observable = client.subscribe(options); } const sub = observable.subscribe(observer); return () => sub.unsubscribe(); diff --git a/src/react/types/types.documentation.ts b/src/react/types/types.documentation.ts index c5f232c1b18..515834e7cb7 100644 --- a/src/react/types/types.documentation.ts +++ b/src/react/types/types.documentation.ts @@ -552,6 +552,11 @@ export interface SubscriptionOptionsDocumentation { */ context: unknown; + /** + * Shared context between your component and your network interface (Apollo Link). + */ + extensions: unknown; + /** * Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component completes the subscription. * diff --git a/src/react/types/types.ts b/src/react/types/types.ts index ee441640737..cd2e6df1ccb 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -449,6 +449,8 @@ export interface BaseSubscriptionOptions< skip?: boolean; /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#context:member} */ context?: DefaultContext; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#extensions:member} */ + extensions?: Record; /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onComplete:member} */ onComplete?: () => void; /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onData:member} */ From a768575ac1454587208aad63abc811b6a966fe72 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Tue, 9 Jul 2024 12:41:24 -0600 Subject: [PATCH 48/62] Deprecate experimental testing utilities (#11930) --- .api-reports/api-report-testing_experimental.api.md | 4 ++-- .changeset/weak-ads-develop.md | 5 +++++ src/testing/experimental/createSchemaFetch.ts | 2 ++ src/testing/experimental/createTestSchema.ts | 2 ++ 4 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/weak-ads-develop.md diff --git a/.api-reports/api-report-testing_experimental.api.md b/.api-reports/api-report-testing_experimental.api.md index 97af4a043cf..330f543fc64 100644 --- a/.api-reports/api-report-testing_experimental.api.md +++ b/.api-reports/api-report-testing_experimental.api.md @@ -8,7 +8,7 @@ import type { FieldNode } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; import type { GraphQLSchema } from 'graphql'; -// @alpha +// @alpha @deprecated export const createSchemaFetch: (schema: GraphQLSchema, mockFetchOpts?: { validate?: boolean; delay?: { @@ -24,7 +24,7 @@ export const createSchemaFetch: (schema: GraphQLSchema, mockFetchOpts?: { // Warning: (ae-forgotten-export) The symbol "TestSchemaOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "ProxiedSchema" needs to be exported by the entry point index.d.ts // -// @alpha +// @alpha @deprecated export const createTestSchema: (schemaWithTypeDefs: GraphQLSchema, options: TestSchemaOptions) => ProxiedSchema; // @public diff --git a/.changeset/weak-ads-develop.md b/.changeset/weak-ads-develop.md new file mode 100644 index 00000000000..2499c7cb871 --- /dev/null +++ b/.changeset/weak-ads-develop.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Deprecates experimental schema testing utilities introduced in 3.10 in favor of recommending [`@apollo/graphql-testing-library`](https://github.com/apollographql/graphql-testing-library). diff --git a/src/testing/experimental/createSchemaFetch.ts b/src/testing/experimental/createSchemaFetch.ts index fe253f98899..9f018156cf1 100644 --- a/src/testing/experimental/createSchemaFetch.ts +++ b/src/testing/experimental/createSchemaFetch.ts @@ -30,6 +30,8 @@ import { wait } from "../core/wait.js"; * ``` * @since 3.10.0 * @alpha + * @deprecated `createSchemaFetch` is deprecated and will be removed in 3.12.0. + * Please migrate to [`@apollo/graphql-testing-library`](https://github.com/apollographql/graphql-testing-library). */ const createSchemaFetch = ( schema: GraphQLSchema, diff --git a/src/testing/experimental/createTestSchema.ts b/src/testing/experimental/createTestSchema.ts index 5a4002c2902..e07dd923e4e 100644 --- a/src/testing/experimental/createTestSchema.ts +++ b/src/testing/experimental/createTestSchema.ts @@ -50,6 +50,8 @@ interface TestSchemaOptions { * ``` * @since 3.9.0 * @alpha + * @deprecated `createTestSchema` is deprecated and will be removed in 3.12.0. + * Please migrate to [`@apollo/graphql-testing-library`](https://github.com/apollographql/graphql-testing-library). */ const createTestSchema = ( schemaWithTypeDefs: GraphQLSchema, From b9371366bf19c9c6b0615cc27b34dfaf9675901f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 19:53:34 +0000 Subject: [PATCH 49/62] Prepare for rc release --- .changeset/pre.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 958803ab852..819d16d3797 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,8 +1,8 @@ { "mode": "pre", - "tag": "alpha", + "tag": "rc", "initialVersions": { "@apollo/client": "3.10.8" }, "changesets": [] -} +} \ No newline at end of file From d9f9d15f8aebfa9121fbfdf8a79b2d06066590f6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 9 Jul 2024 14:10:52 -0600 Subject: [PATCH 50/62] Version Packages (rc) (#11940) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 20 ++++++++++++-- CHANGELOG.md | 64 ++++++++++++++++++++++++++++++++++++++++++--- package-lock.json | 4 +-- package.json | 2 +- 4 files changed, 81 insertions(+), 9 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 819d16d3797..cadce124544 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -4,5 +4,21 @@ "initialVersions": { "@apollo/client": "3.10.8" }, - "changesets": [] -} \ No newline at end of file + "changesets": [ + "angry-ravens-mate", + "angry-seals-jog", + "chilly-dots-shake", + "clever-bikes-admire", + "flat-onions-guess", + "fluffy-badgers-rush", + "little-suits-return", + "nasty-olives-act", + "pink-ants-remember", + "slimy-balloons-cheat", + "slimy-berries-yawn", + "tasty-chairs-dress", + "thin-lies-begin", + "unlucky-birds-press", + "weak-ads-develop" + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index cea036ef5be..a59d2872174 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,61 @@ # @apollo/client +## 3.11.0-rc.0 + +### Minor Changes + +- [#11923](https://github.com/apollographql/apollo-client/pull/11923) [`d88c7f8`](https://github.com/apollographql/apollo-client/commit/d88c7f8909e3cb31532e8b1fc7dd06be12f35591) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add support for `subscribeToMore` function to `useQueryRefHandlers`. + +- [#11854](https://github.com/apollographql/apollo-client/pull/11854) [`3812800`](https://github.com/apollographql/apollo-client/commit/3812800c6e4e5e3e64f473543babdba35ce100c2) Thanks [@jcostello-atlassian](https://github.com/jcostello-atlassian)! - Support extensions in useSubscription + +- [#11923](https://github.com/apollographql/apollo-client/pull/11923) [`d88c7f8`](https://github.com/apollographql/apollo-client/commit/d88c7f8909e3cb31532e8b1fc7dd06be12f35591) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add support for `subscribeToMore` function to `useLoadableQuery`. + +- [#11863](https://github.com/apollographql/apollo-client/pull/11863) [`98e44f7`](https://github.com/apollographql/apollo-client/commit/98e44f74cb7c7e93a81bdc7492c9218bf4a2dcd4) Thanks [@phryneas](https://github.com/phryneas)! - Reimplement `useSubscription` to fix rules of React violations. + +- [#11869](https://github.com/apollographql/apollo-client/pull/11869) [`a69327c`](https://github.com/apollographql/apollo-client/commit/a69327cce1b36e8855258e9b19427511e0af8748) Thanks [@phryneas](https://github.com/phryneas)! - Rewrite big parts of `useQuery` and `useLazyQuery` to be more compliant with the Rules of React and React Compiler + +- [#11936](https://github.com/apollographql/apollo-client/pull/11936) [`1b23337`](https://github.com/apollographql/apollo-client/commit/1b23337e5a9eec4ce3ed69531ca4f4afe8e897a6) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add the ability to specify a name for the client instance for use with Apollo Client Devtools. This is useful when instantiating multiple clients to identify the client instance more easily. This deprecates the `connectToDevtools` option in favor of a new `devtools` configuration. + + ```ts + new ApolloClient({ + devtools: { + enabled: true, + name: "Test Client", + }, + }); + ``` + + This option is backwards-compatible with `connectToDevtools` and will be used in the absense of a `devtools` option. + +- [#11923](https://github.com/apollographql/apollo-client/pull/11923) [`d88c7f8`](https://github.com/apollographql/apollo-client/commit/d88c7f8909e3cb31532e8b1fc7dd06be12f35591) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add support for `subscribeToMore` function to `useBackgroundQuery`. + +- [#11789](https://github.com/apollographql/apollo-client/pull/11789) [`5793301`](https://github.com/apollographql/apollo-client/commit/579330147d6bd6f7167a35413a33746103e375cb) Thanks [@phryneas](https://github.com/phryneas)! - Changes usages of the `GraphQLError` type to `GraphQLFormattedError`. + + This was a type bug - these errors were never `GraphQLError` instances + to begin with, and the `GraphQLError` class has additional properties that can + never be correctly rehydrated from a GraphQL result. + The correct type to use here is `GraphQLFormattedError`. + + Similarly, please ensure to use the type `FormattedExecutionResult` + instead of `ExecutionResult` - the non-"Formatted" versions of these types + are for use on the server only, but don't get transported over the network. + +- [#11930](https://github.com/apollographql/apollo-client/pull/11930) [`a768575`](https://github.com/apollographql/apollo-client/commit/a768575ac1454587208aad63abc811b6a966fe72) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Deprecates experimental schema testing utilities introduced in 3.10 in favor of recommending [`@apollo/graphql-testing-library`](https://github.com/apollographql/graphql-testing-library). + +### Patch Changes + +- [#11927](https://github.com/apollographql/apollo-client/pull/11927) [`2941824`](https://github.com/apollographql/apollo-client/commit/2941824dd66cdd20eee5f2293373ad7a9cf991a4) Thanks [@phryneas](https://github.com/phryneas)! - Add `restart` function to `useSubscription`. + +- [#11902](https://github.com/apollographql/apollo-client/pull/11902) [`96422ce`](https://github.com/apollographql/apollo-client/commit/96422ce95b923b560321a88acd2eec35cf2a1c18) Thanks [@phryneas](https://github.com/phryneas)! - Add `cause` field to `ApolloError`. + +- [#11806](https://github.com/apollographql/apollo-client/pull/11806) [`8df6013`](https://github.com/apollographql/apollo-client/commit/8df6013b6b45452ec058fab3e068b5b6d6c493f7) Thanks [@phryneas](https://github.com/phryneas)! - MockLink: add query default variables if not specified in mock request + +- [#11626](https://github.com/apollographql/apollo-client/pull/11626) [`228429a`](https://github.com/apollographql/apollo-client/commit/228429a1d36eae691473b24fb641ec3cd84c8a3d) Thanks [@phryneas](https://github.com/phryneas)! - Call `nextFetchPolicy` with "variables-changed" even if there is a `fetchPolicy` specified. (fixes #11365) + +- [#11719](https://github.com/apollographql/apollo-client/pull/11719) [`09a6677`](https://github.com/apollographql/apollo-client/commit/09a6677ec1a0cffedeecb2cbac5cd3a3c8aa0fa1) Thanks [@phryneas](https://github.com/phryneas)! - Allow wrapping `createQueryPreloader` + +- [#11921](https://github.com/apollographql/apollo-client/pull/11921) [`70406bf`](https://github.com/apollographql/apollo-client/commit/70406bfd2b9a645d781638569853d9b435e047df) Thanks [@phryneas](https://github.com/phryneas)! - add `ignoreResults` option to `useSubscription` + ## 3.10.8 ### Patch Changes @@ -457,7 +513,7 @@ import { Environment, Network, RecordSource, Store } from "relay-runtime"; const fetchMultipartSubs = createFetchMultipartSubscription( - "http://localhost:4000", + "http://localhost:4000" ); const network = Network.create(fetchQuery, fetchMultipartSubs); @@ -810,7 +866,7 @@ return data.breeds.map(({ characteristics }) => characteristics.map((characteristic) => (
{characteristic}
- )), + )) ); } ``` @@ -861,7 +917,7 @@ const { data } = useSuspenseQuery( query, - id ? { variables: { id } } : skipToken, + id ? { variables: { id } } : skipToken ); ``` @@ -2816,7 +2872,7 @@ In upcoming v3.6.x and v3.7 (beta) releases, we will be completely overhauling o fields: { comments(comments: Reference[], { readField }) { return comments.filter( - (comment) => idToRemove !== readField("id", comment), + (comment) => idToRemove !== readField("id", comment) ); }, }, diff --git a/package-lock.json b/package-lock.json index 7f208bd5e57..fc3325bf1b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.10.8", + "version": "3.11.0-rc.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.10.8", + "version": "3.11.0-rc.0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index ad69cdf8d52..190ae601677 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.10.8", + "version": "3.11.0-rc.0", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 7d833b80119a991e6d2eb58f2c71074d697b8e63 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 10 Jul 2024 08:43:11 -0600 Subject: [PATCH 51/62] Fix issue accessing mutations from devtools (#11946) --- .changeset/hungry-rings-help.md | 5 +++++ .size-limits.json | 4 ++-- src/core/ApolloClient.ts | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 .changeset/hungry-rings-help.md diff --git a/.changeset/hungry-rings-help.md b/.changeset/hungry-rings-help.md new file mode 100644 index 00000000000..d5fa17d2f35 --- /dev/null +++ b/.changeset/hungry-rings-help.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix issue where mutations were not accessible by Apollo Client Devtools in 3.11.0-rc.0. diff --git a/.size-limits.json b/.size-limits.json index 3e400e47793..d8cc123efe5 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 40179, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32973 + "dist/apollo-client.min.cjs": 40185, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32977 } diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 39fc6f3d503..b9c5c7883fc 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -302,7 +302,7 @@ export class ApolloClient implements DataProxy { localState: this.localState, assumeImmutableResults, onBroadcast: - connectToDevTools ? + this.devtoolsConfig.enabled ? () => { if (this.devToolsHookCb) { this.devToolsHookCb({ From 3dd64324dc5156450cead27f8141ea93315ffe65 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 10 Jul 2024 21:25:02 +0200 Subject: [PATCH 52/62] `watchFragment`: forward additional options to `diffOptions` (#11926) * `watchFragment`: forward additional options to `diffOptions` fixes #11924 * chore: bump .size-limits.json * chore: bump .size-limits.json * Clean up Prettier, Size-limit, and Api-Extractor --------- Co-authored-by: Alessia Bellisario Co-authored-by: alessbell --- .changeset/good-suns-happen.md | 5 ++ .size-limits.json | 4 +- src/__tests__/ApolloClient.ts | 96 ++++++++++++++++++++++++++++++++++ src/cache/core/cache.ts | 9 +++- 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 .changeset/good-suns-happen.md diff --git a/.changeset/good-suns-happen.md b/.changeset/good-suns-happen.md new file mode 100644 index 00000000000..e184b2c1d09 --- /dev/null +++ b/.changeset/good-suns-happen.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`watchFragment`: forward additional options to `diffOptions` diff --git a/.size-limits.json b/.size-limits.json index d8cc123efe5..c6da4a36f8f 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 40185, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32977 + "dist/apollo-client.min.cjs": 40206, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32999 } diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 8ba8ccf7ab5..599a29f9a39 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -2419,6 +2419,102 @@ describe("ApolloClient", () => { new Error("Timeout waiting for next event") ); }); + it("works with `variables`", async () => { + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + }); + const ItemFragment = gql` + fragment ItemFragment on Item { + id + text(language: $language) + } + `; + + cache.writeFragment({ + fragment: ItemFragment, + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + variables: { language: "Esperanto" }, + }); + + const observable = client.watchFragment({ + fragment: ItemFragment, + from: { __typename: "Item", id: 5 }, + variables: { language: "Esperanto" }, + }); + + const stream = new ObservableStream(observable); + + { + const result = await stream.takeNext(); + + expect(result).toStrictEqual({ + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + complete: true, + }); + } + }); + it("supports the @includes directive with `variables`", async () => { + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + }); + const ItemFragment = gql` + fragment ItemFragment on Item { + id + text @include(if: $withText) + } + `; + + cache.writeFragment({ + fragment: ItemFragment, + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + variables: { withText: true }, + }); + cache.writeFragment({ + fragment: ItemFragment, + data: { + __typename: "Item", + id: 5, + }, + variables: { withText: false }, + }); + + const observable = client.watchFragment({ + fragment: ItemFragment, + from: { __typename: "Item", id: 5 }, + variables: { withText: true }, + }); + + const stream = new ObservableStream(observable); + + { + const result = await stream.takeNext(); + + expect(result).toStrictEqual({ + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + complete: true, + }); + } + }); }); describe("defaultOptions", () => { diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index d25c5928332..f4b7dd9b599 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -228,10 +228,17 @@ export abstract class ApolloCache implements DataProxy { public watchFragment( options: WatchFragmentOptions ): Observable> { - const { fragment, fragmentName, from, optimistic = true } = options; + const { + fragment, + fragmentName, + from, + optimistic = true, + ...otherOptions + } = options; const query = this.getFragmentDoc(fragment, fragmentName); const diffOptions: Cache.DiffOptions = { + ...otherOptions, returnPartialData: true, id: typeof from === "string" ? from : this.identify(from), query, From 45289186bcaaa33dfe904913eb6df31e2541c219 Mon Sep 17 00:00:00 2001 From: Alessia Bellisario Date: Wed, 10 Jul 2024 15:51:39 -0400 Subject: [PATCH 53/62] remove deprecated `watchFragment` option, `canonizeResults` (#11949) * fix: remove deprecated watchFragment option canonizeResults * chore: add changeset * chore: update api-reports --- .api-reports/api-report-cache.api.md | 2 -- .api-reports/api-report-core.api.md | 2 -- .api-reports/api-report-react.api.md | 2 -- .api-reports/api-report-react_components.api.md | 2 -- .api-reports/api-report-react_context.api.md | 2 -- .api-reports/api-report-react_hoc.api.md | 2 -- .api-reports/api-report-react_hooks.api.md | 2 -- .api-reports/api-report-react_internal.api.md | 2 -- .api-reports/api-report-react_ssr.api.md | 2 -- .api-reports/api-report-testing.api.md | 2 -- .api-reports/api-report-testing_core.api.md | 2 -- .api-reports/api-report-utilities.api.md | 2 -- .api-reports/api-report.api.md | 2 -- .changeset/curly-vans-draw.md | 5 +++++ src/cache/core/cache.ts | 11 ----------- 15 files changed, 5 insertions(+), 37 deletions(-) create mode 100644 .changeset/curly-vans-draw.md diff --git a/.api-reports/api-report-cache.api.md b/.api-reports/api-report-cache.api.md index 30ec666b306..3434c4bbb06 100644 --- a/.api-reports/api-report-cache.api.md +++ b/.api-reports/api-report-cache.api.md @@ -976,8 +976,6 @@ export type TypePolicy = { // @public export interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 5382952aee8..237eac1a463 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -2231,8 +2231,6 @@ export interface UriFunction { // @public export interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 0e32289572a..5b8b33b8ae6 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -2285,8 +2285,6 @@ TVariables // @public interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index e8ed84f1def..24881bcaf5d 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -1764,8 +1764,6 @@ interface UriFunction { // @public interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index 6f255e1efa0..fa3fd404a7d 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -1684,8 +1684,6 @@ interface UriFunction { // @public interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index 7b8cdd3f2fb..6be8fbce071 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -1696,8 +1696,6 @@ interface UriFunction { // @public interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index 6c8829f614c..0691c35d9ba 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -2109,8 +2109,6 @@ export interface UseSuspenseQueryResult { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 934334ea9b9..98443e3b6b8 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -2127,8 +2127,6 @@ TVariables // @public interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index c547e869238..eb50183646e 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1669,8 +1669,6 @@ interface UriFunction { // @public interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index 2d72d09c0ae..ebd148cf04e 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -1728,8 +1728,6 @@ export function wait(ms: number): Promise; // @public interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index ddf2c0aa6c4..ef631e90da1 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1685,8 +1685,6 @@ export function wait(ms: number): Promise; // @public interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index ea225f4f7e6..ab3858c6f0d 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -2590,8 +2590,6 @@ export type VariableValue = (node: VariableNode) => any; // @public interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index 58d5f5cbdd7..ef88362554a 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2946,8 +2946,6 @@ TVariables // @public export interface WatchFragmentOptions { - // @deprecated (undocumented) - canonizeResults?: boolean; fragment: DocumentNode | TypedDocumentNode; fragmentName?: string; from: StoreObject | Reference | string; diff --git a/.changeset/curly-vans-draw.md b/.changeset/curly-vans-draw.md new file mode 100644 index 00000000000..e9096914b90 --- /dev/null +++ b/.changeset/curly-vans-draw.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Remove deprecated `watchFragment` option, `canonizeResults` diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index f4b7dd9b599..cb953152c45 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -69,17 +69,6 @@ export interface WatchFragmentOptions { * @docGroup 2. Cache options */ optimistic?: boolean; - /** - * @deprecated - * Using `canonizeResults` can result in memory leaks so we generally do not - * recommend using this option anymore. - * A future version of Apollo Client will contain a similar feature. - * - * Whether to canonize cache results before returning them. Canonization - * takes some extra time, but it speeds up future deep equality comparisons. - * Defaults to false. - */ - canonizeResults?: boolean; } /** From a3bbabe988606820bc4ef507c60a7327676b0ae9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jul 2024 16:33:46 -0600 Subject: [PATCH 54/62] Version Packages (rc) (#11947) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 3 +++ CHANGELOG.md | 10 ++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index cadce124544..2f8559cf695 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -9,8 +9,11 @@ "angry-seals-jog", "chilly-dots-shake", "clever-bikes-admire", + "curly-vans-draw", "flat-onions-guess", "fluffy-badgers-rush", + "good-suns-happen", + "hungry-rings-help", "little-suits-return", "nasty-olives-act", "pink-ants-remember", diff --git a/CHANGELOG.md b/CHANGELOG.md index a59d2872174..03ebf678445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # @apollo/client +## 3.11.0-rc.1 + +### Patch Changes + +- [#11949](https://github.com/apollographql/apollo-client/pull/11949) [`4528918`](https://github.com/apollographql/apollo-client/commit/45289186bcaaa33dfe904913eb6df31e2541c219) Thanks [@alessbell](https://github.com/alessbell)! - Remove deprecated `watchFragment` option, `canonizeResults` + +- [#11926](https://github.com/apollographql/apollo-client/pull/11926) [`3dd6432`](https://github.com/apollographql/apollo-client/commit/3dd64324dc5156450cead27f8141ea93315ffe65) Thanks [@phryneas](https://github.com/phryneas)! - `watchFragment`: forward additional options to `diffOptions` + +- [#11946](https://github.com/apollographql/apollo-client/pull/11946) [`7d833b8`](https://github.com/apollographql/apollo-client/commit/7d833b80119a991e6d2eb58f2c71074d697b8e63) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix issue where mutations were not accessible by Apollo Client Devtools in 3.11.0-rc.0. + ## 3.11.0-rc.0 ### Minor Changes diff --git a/package-lock.json b/package-lock.json index fc3325bf1b4..25422172c98 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.11.0-rc.0", + "version": "3.11.0-rc.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.11.0-rc.0", + "version": "3.11.0-rc.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 190ae601677..7fead782a5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.11.0-rc.0", + "version": "3.11.0-rc.1", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 78332be32a9af0da33eb3e4100e7a76c3eac2496 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 11 Jul 2024 14:16:27 +0200 Subject: [PATCH 55/62] correctly test for error equality on jest (#11937) * correctly test for error equality on jest * adjust tests, change schemafetch error * linting * enforce matching `ApolloError` instances in tests everywhere * size * also always enforce stricter checks for `GraphQLError` --- .changeset/early-tips-vanish.md | 5 + package-lock.json | 370 +----------------- package.json | 5 +- patches/pretty-format+29.7.0.patch | 14 + src/__tests__/ApolloClient.ts | 4 +- .../graphqlSubscriptions.ts.snap | 66 +++- src/__tests__/client.ts | 2 +- src/config/jest/areApolloErrorsEqual.ts | 26 ++ src/config/jest/areGraphQlErrorsEqual.ts | 12 + src/config/jest/setup.ts | 5 + src/core/__tests__/ObservableQuery.ts | 11 +- .../__tests__/client/Mutation.test.tsx | 24 +- .../__tests__/client/Query.test.tsx | 4 +- .../__tests__/client/Subscription.test.tsx | 6 +- .../hooks/__tests__/useLazyQuery.test.tsx | 16 +- .../hooks/__tests__/useMutation.test.tsx | 19 +- .../__tests__/createTestSchema.test.tsx | 8 +- src/testing/experimental/createSchemaFetch.ts | 19 +- .../react/__tests__/MockedProvider.test.tsx | 5 +- .../MockedProvider.test.tsx.snap | 179 ++++++++- 20 files changed, 378 insertions(+), 422 deletions(-) create mode 100644 .changeset/early-tips-vanish.md create mode 100644 patches/pretty-format+29.7.0.patch create mode 100644 src/config/jest/areApolloErrorsEqual.ts create mode 100644 src/config/jest/areGraphQlErrorsEqual.ts diff --git a/.changeset/early-tips-vanish.md b/.changeset/early-tips-vanish.md new file mode 100644 index 00000000000..3c794284f8e --- /dev/null +++ b/.changeset/early-tips-vanish.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`createSchemaFetch`: simulate serialized errors instead of an `ApolloError` instance diff --git a/package-lock.json b/package-lock.json index 25422172c98..c9065d07452 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2233,38 +2233,6 @@ } } }, - "node_modules/@jest/core/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@jest/core/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/core/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/@jest/environment": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", @@ -3567,38 +3535,6 @@ "pretty-format": "^29.0.0" } }, - "node_modules/@types/jest/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@types/jest/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@types/jest/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/@types/jsdom": { "version": "20.0.0", "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-20.0.0.tgz", @@ -8109,18 +8045,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-circus/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/jest-circus/node_modules/p-limit": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", @@ -8136,26 +8060,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-circus/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-circus/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/jest-cli": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", @@ -8234,18 +8138,6 @@ } } }, - "node_modules/jest-config/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/jest-config/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8266,26 +8158,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/jest-config/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-config/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/jest-diff": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", @@ -8301,38 +8173,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-diff/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-diff/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-diff/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/jest-docblock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", @@ -8361,38 +8201,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-each/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-each/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-each/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/jest-environment-jsdom": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-29.7.0.tgz", @@ -8499,38 +8307,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-leak-detector/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-leak-detector/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-leak-detector/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/jest-matcher-utils": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", @@ -8546,38 +8322,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-matcher-utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-matcher-utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/jest-message-util": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", @@ -8598,38 +8342,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-message-util/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/jest-mock": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", @@ -8844,38 +8556,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/jest-snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-snapshot/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/jest-snapshot/node_modules/semver": { "version": "7.6.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", @@ -8922,18 +8602,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-validate/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/jest-validate/node_modules/camelcase": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", @@ -8946,26 +8614,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-validate/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-validate/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/jest-watcher": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", @@ -10435,17 +10083,17 @@ } }, "node_modules/pretty-format": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", - "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", "dev": true, "dependencies": { - "ansi-regex": "^5.0.1", + "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" + "react-is": "^18.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/pretty-format/node_modules/ansi-styles": { @@ -10461,9 +10109,9 @@ } }, "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true }, "node_modules/prompts": { diff --git a/package.json b/package.json index 7fead782a5e..6bea971ff0a 100644 --- a/package.json +++ b/package.json @@ -204,5 +204,8 @@ "**/*.js.map", "**/*.d.ts", "**/*.json" - ] + ], + "overrides": { + "pretty-format": "^29.7.0" + } } diff --git a/patches/pretty-format+29.7.0.patch b/patches/pretty-format+29.7.0.patch new file mode 100644 index 00000000000..d3d733f80fb --- /dev/null +++ b/patches/pretty-format+29.7.0.patch @@ -0,0 +1,14 @@ +diff --git a/node_modules/pretty-format/build/index.js b/node_modules/pretty-format/build/index.js +index 8d3a562..879eed7 100644 +--- a/node_modules/pretty-format/build/index.js ++++ b/node_modules/pretty-format/build/index.js +@@ -103,6 +103,9 @@ function printBasicValue(val, printFunctionName, escapeRegex, escapeString) { + if (val === null) { + return 'null'; + } ++ if (val.name === 'ApolloError' || val.name === 'GraphQLError') { ++ return null ++ } + const typeOf = typeof val; + if (typeOf === 'number') { + return printNumber(val); diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 599a29f9a39..bcf9978ef90 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -2735,7 +2735,9 @@ describe("ApolloClient", () => { expect(invariantDebugSpy).toHaveBeenCalledTimes(1); expect(invariantDebugSpy).toHaveBeenCalledWith( "In client.refetchQueries, Promise.all promise rejected with error %o", - new ApolloError({ errorMessage: "refetch failed" }) + new ApolloError({ + networkError: new Error("refetch failed"), + }) ); resolve(); } catch (err) { diff --git a/src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap b/src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap index c32d90c0954..cec1ed33bf9 100644 --- a/src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap +++ b/src/__tests__/__snapshots__/graphqlSubscriptions.ts.snap @@ -1,5 +1,67 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`GraphQL Subscriptions should throw an error if the result has errors on it 1`] = `[ApolloError: This is an error]`; +exports[`GraphQL Subscriptions should throw an error if the result has errors on it 1`] = ` +ApolloError { + "cause": Object { + "locations": Array [ + Object { + "column": 3, + "line": 2, + }, + ], + "message": "This is an error", + "path": Array [ + "result", + ], + }, + "clientErrors": Array [], + "extraInfo": undefined, + "graphQLErrors": Array [ + Object { + "locations": Array [ + Object { + "column": 3, + "line": 2, + }, + ], + "message": "This is an error", + "path": Array [ + "result", + ], + }, + ], + "message": "This is an error", + "name": "ApolloError", + "networkError": null, + "protocolErrors": Array [], +} +`; -exports[`GraphQL Subscriptions should throw an error if the result has protocolErrors on it 1`] = `[ApolloError: cannot read message from websocket]`; +exports[`GraphQL Subscriptions should throw an error if the result has protocolErrors on it 1`] = ` +ApolloError { + "cause": Object { + "extensions": Array [ + Object { + "code": "WEBSOCKET_MESSAGE_ERROR", + }, + ], + "message": "cannot read message from websocket", + }, + "clientErrors": Array [], + "extraInfo": undefined, + "graphQLErrors": Array [], + "message": "cannot read message from websocket", + "name": "ApolloError", + "networkError": null, + "protocolErrors": Array [ + Object { + "extensions": Array [ + Object { + "code": "WEBSOCKET_MESSAGE_ERROR", + }, + ], + "message": "cannot read message from websocket", + }, + ], +} +`; diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index c16b01d593e..fd49358b6d4 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -2855,7 +2855,7 @@ describe("client", () => { const lastError = observable.getLastError(); expect(lastError).toBeInstanceOf(ApolloError); - expect(lastError!.networkError).toEqual(error); + expect(lastError!.networkError).toEqual((error as any).networkError); const lastResult = observable.getLastResult(); expect(lastResult).toBeTruthy(); diff --git a/src/config/jest/areApolloErrorsEqual.ts b/src/config/jest/areApolloErrorsEqual.ts new file mode 100644 index 00000000000..0724c023986 --- /dev/null +++ b/src/config/jest/areApolloErrorsEqual.ts @@ -0,0 +1,26 @@ +import type { ApolloError } from "../../errors/index.js"; +import type { Tester } from "@jest/expect-utils"; +function isApolloError(e: any): e is ApolloError { + return e instanceof Error && e.name == "ApolloError"; +} + +export const areApolloErrorsEqual: Tester = function (a, b, customTesters) { + const isAApolloError = isApolloError(a); + const isBApolloError = isApolloError(b); + + if (isAApolloError && isBApolloError) { + return ( + a.message === b.message && + this.equals(a.graphQLErrors, b.graphQLErrors, customTesters) && + this.equals(a.protocolErrors, b.protocolErrors, customTesters) && + this.equals(a.clientErrors, b.clientErrors, customTesters) && + this.equals(a.networkError, b.networkError, customTesters) && + this.equals(a.cause, b.cause, customTesters) && + this.equals(a.extraInfo, b.extraInfo, customTesters) + ); + } else if (isAApolloError === isBApolloError) { + return undefined; + } else { + return false; + } +}; diff --git a/src/config/jest/areGraphQlErrorsEqual.ts b/src/config/jest/areGraphQlErrorsEqual.ts new file mode 100644 index 00000000000..44b681228ed --- /dev/null +++ b/src/config/jest/areGraphQlErrorsEqual.ts @@ -0,0 +1,12 @@ +import { GraphQLError } from "graphql"; +import type { Tester } from "@jest/expect-utils"; + +export const areGraphQLErrorsEqual: Tester = function (a, b, customTesters) { + if (a instanceof GraphQLError || b instanceof GraphQLError) { + return this.equals( + a instanceof GraphQLError ? a.toJSON() : a, + b instanceof GraphQLError ? b.toJSON() : b, + customTesters + ); + } +}; diff --git a/src/config/jest/setup.ts b/src/config/jest/setup.ts index f5ad519835f..f5b6a3e2496 100644 --- a/src/config/jest/setup.ts +++ b/src/config/jest/setup.ts @@ -2,6 +2,8 @@ import gql from "graphql-tag"; import "@testing-library/jest-dom"; import { loadErrorMessageHandler } from "../../dev/loadErrorMessageHandler.js"; import "../../testing/matchers/index.js"; +import { areApolloErrorsEqual } from "./areApolloErrorsEqual.js"; +import { areGraphQLErrorsEqual } from "./areGraphQlErrorsEqual.js"; // Turn off warnings for repeated fragment names gql.disableFragmentWarnings(); @@ -27,3 +29,6 @@ if (!Symbol.asyncDispose) { value: Symbol("asyncDispose"), }); } + +// @ts-ignore +expect.addEqualityTesters([areApolloErrorsEqual, areGraphQLErrorsEqual]); diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index 98c1f735026..b103fb38915 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -92,6 +92,9 @@ describe("ObservableQuery", () => { const error = new GraphQLError("is offline.", undefined, null, null, [ "people_one", ]); + const wrappedError = new ApolloError({ + graphQLErrors: [error], + }); const createQueryManager = ({ link }: { link: ApolloLink }) => { return new QueryManager({ @@ -2136,9 +2139,9 @@ describe("ObservableQuery", () => { .result() .then(() => reject("Observable did not error when it should have")) .catch((currentError) => { - expect(currentError).toEqual(error); + expect(currentError).toEqual(wrappedError); const lastError = observable.getLastError(); - expect(lastError).toEqual(error); + expect(lastError).toEqual(wrappedError); resolve(); }) .catch(reject); @@ -2177,9 +2180,9 @@ describe("ObservableQuery", () => { ) ) .catch((currentError) => { - expect(currentError).toEqual(error); + expect(currentError).toEqual(wrappedError); const lastError = observable.getLastError(); - expect(lastError).toEqual(error); + expect(lastError).toEqual(wrappedError); resolve(); }) .catch(reject) diff --git a/src/react/components/__tests__/client/Mutation.test.tsx b/src/react/components/__tests__/client/Mutation.test.tsx index fc88f76b44e..7a88e64b5ae 100644 --- a/src/react/components/__tests__/client/Mutation.test.tsx +++ b/src/react/components/__tests__/client/Mutation.test.tsx @@ -305,7 +305,9 @@ describe("General Mutation testing", () => { {(createTodo: any) => { if (!called) { createTodo().catch((error: any) => { - expect(error).toEqual(new Error("Error 1")); + expect(error).toEqual( + new ApolloError({ networkError: new Error("Error 1") }) + ); done = true; }); } @@ -382,9 +384,13 @@ describe("General Mutation testing", () => { const onError = (error: Error) => { if (count === 1) { - expect(error).toEqual(new Error("Error 1")); + expect(error).toEqual( + new ApolloError({ networkError: new Error("Error 1") }) + ); } else if (count === 3) { - expect(error).toEqual(new Error("Error 2")); + expect(error).toEqual( + new ApolloError({ networkError: new Error("Error 2") }) + ); } }; const Component = () => ( @@ -402,7 +408,9 @@ describe("General Mutation testing", () => { expect(result.loading).toEqual(false); expect(result.data).toEqual(undefined); expect(result.called).toEqual(true); - expect(result.error).toEqual(new Error("Error 2")); + expect(result.error).toEqual( + new ApolloError({ networkError: new Error("Error 2") }) + ); } count++; return
; @@ -499,12 +507,16 @@ describe("General Mutation testing", () => { {(createTodo: any, result: any) => { if (count === 0) { createTodo().catch((err: any) => { - expect(err).toEqual(new Error("error occurred")); + expect(err).toEqual( + new ApolloError({ networkError: new Error("error occurred") }) + ); }); } else if (count === 1) { expect(result.loading).toBeTruthy(); } else if (count === 2) { - expect(result.error).toEqual(new Error("error occurred")); + expect(result.error).toEqual( + new ApolloError({ networkError: new Error("error occurred") }) + ); } count++; return
; diff --git a/src/react/components/__tests__/client/Query.test.tsx b/src/react/components/__tests__/client/Query.test.tsx index 9182e61e19e..d8027c0c509 100644 --- a/src/react/components/__tests__/client/Query.test.tsx +++ b/src/react/components/__tests__/client/Query.test.tsx @@ -213,7 +213,9 @@ describe("Query component", () => { return null; } try { - expect(result.error).toEqual(new Error("error occurred")); + expect(result.error).toEqual( + new ApolloError({ networkError: new Error("error occurred") }) + ); finished = true; } catch (error) { reject(error); diff --git a/src/react/components/__tests__/client/Subscription.test.tsx b/src/react/components/__tests__/client/Subscription.test.tsx index ec4deaa629c..1604623810f 100644 --- a/src/react/components/__tests__/client/Subscription.test.tsx +++ b/src/react/components/__tests__/client/Subscription.test.tsx @@ -2,7 +2,7 @@ import React from "react"; import gql from "graphql-tag"; import { render, waitFor } from "@testing-library/react"; -import { ApolloClient } from "../../../../core"; +import { ApolloClient, ApolloError } from "../../../../core"; import { InMemoryCache as Cache } from "../../../../cache"; import { ApolloProvider } from "../../../context"; import { ApolloLink, DocumentNode, Operation } from "../../../../link/core"; @@ -390,7 +390,9 @@ itAsync("renders an error", (resolve, reject) => { expect(error).toBeUndefined(); } else if (count === 1) { expect(loading).toBe(false); - expect(error).toEqual(new Error("error occurred")); + expect(error).toEqual( + new ApolloError({ protocolErrors: [new Error("error occurred")] }) + ); expect(data).toBeUndefined(); } } catch (error) { diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx index 08f94df5c60..df38ea6adc0 100644 --- a/src/react/hooks/__tests__/useLazyQuery.test.tsx +++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx @@ -1069,14 +1069,14 @@ describe("useLazyQuery Hook", () => { { request: { query: helloQuery }, result: { - errors: [new GraphQLError("error 1")], + errors: [{ message: "error 1" }], }, delay: 20, }, { request: { query: helloQuery }, result: { - errors: [new GraphQLError("error 2")], + errors: [{ message: "error 2" }], }, delay: 20, }, @@ -1111,7 +1111,9 @@ describe("useLazyQuery Hook", () => { const [, result] = await ProfiledHook.takeSnapshot(); expect(result.loading).toBe(false); expect(result.data).toBeUndefined(); - expect(result.error).toEqual(new Error("error 1")); + expect(result.error).toEqual( + new ApolloError({ graphQLErrors: [{ message: "error 1" }] }) + ); } await executePromise.then((result) => { @@ -1126,14 +1128,18 @@ describe("useLazyQuery Hook", () => { const [, result] = await ProfiledHook.takeSnapshot(); expect(result.loading).toBe(true); expect(result.data).toBeUndefined(); - expect(result.error).toEqual(new Error("error 1")); + expect(result.error).toEqual( + new ApolloError({ graphQLErrors: [{ message: "error 1" }] }) + ); } { const [, result] = await ProfiledHook.takeSnapshot(); expect(result.loading).toBe(false); expect(result.data).toBeUndefined(); - expect(result.error).toEqual(new Error("error 2")); + expect(result.error).toEqual( + new ApolloError({ graphQLErrors: [{ message: "error 2" }] }) + ); } }); diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx index 452b1ad77de..da26fd2c87d 100644 --- a/src/react/hooks/__tests__/useMutation.test.tsx +++ b/src/react/hooks/__tests__/useMutation.test.tsx @@ -8,6 +8,7 @@ import fetchMock from "fetch-mock"; import { ApolloClient, + ApolloError, ApolloLink, ApolloQueryResult, Cache, @@ -346,7 +347,7 @@ describe("useMutation Hook", () => { variables, }, result: { - errors: [new GraphQLError(CREATE_TODO_ERROR)], + errors: [{ message: CREATE_TODO_ERROR }], }, }, ]; @@ -371,7 +372,9 @@ describe("useMutation Hook", () => { throw new Error("function did not error"); }); - expect(fetchError).toEqual(new GraphQLError(CREATE_TODO_ERROR)); + expect(fetchError).toEqual( + new ApolloError({ graphQLErrors: [{ message: CREATE_TODO_ERROR }] }) + ); }); it(`should reject when errorPolicy is 'none'`, async () => { @@ -961,14 +964,13 @@ describe("useMutation Hook", () => { expect(fetchResult).toEqual({ data: undefined, - // Not sure why we unwrap errors here. - errors: errors[0], + errors: new ApolloError({ graphQLErrors: errors }), }); expect(onCompleted).toHaveBeenCalledTimes(0); expect(onError).toHaveBeenCalledTimes(1); expect(onError).toHaveBeenCalledWith( - errors[0], + new ApolloError({ graphQLErrors: errors }), expect.objectContaining({ variables }) ); }); @@ -1012,7 +1014,7 @@ describe("useMutation Hook", () => { }); it("should allow updating onError while mutation is executing", async () => { - const errors = [new GraphQLError(CREATE_TODO_ERROR)]; + const errors = [{ message: CREATE_TODO_ERROR }]; const variables = { priority: "Low", description: "Get milk.", @@ -1060,15 +1062,14 @@ describe("useMutation Hook", () => { expect(fetchResult).toEqual({ data: undefined, - // Not sure why we unwrap errors here. - errors: errors[0], + errors: new ApolloError({ graphQLErrors: errors }), }); expect(onCompleted).toHaveBeenCalledTimes(0); expect(onError).toHaveBeenCalledTimes(0); expect(onError1).toHaveBeenCalledTimes(1); expect(onError1).toHaveBeenCalledWith( - errors[0], + new ApolloError({ graphQLErrors: errors }), expect.objectContaining({ variables }) ); }); diff --git a/src/testing/experimental/__tests__/createTestSchema.test.tsx b/src/testing/experimental/__tests__/createTestSchema.test.tsx index 99b579ddde2..60ad1e08bd5 100644 --- a/src/testing/experimental/__tests__/createTestSchema.test.tsx +++ b/src/testing/experimental/__tests__/createTestSchema.test.tsx @@ -13,7 +13,7 @@ import { spyOnConsole, } from "../../internal/index.js"; import { createTestSchema } from "../createTestSchema.js"; -import { GraphQLError, buildSchema } from "graphql"; +import { buildSchema } from "graphql"; import type { UseSuspenseQueryResult } from "../../../react/index.js"; import { useMutation, useSuspenseQuery } from "../../../react/index.js"; import userEvent from "@testing-library/user-event"; @@ -740,7 +740,9 @@ describe("schema proxy", () => { expect(snapshot.error).toEqual( new ApolloError({ - graphQLErrors: [new GraphQLError("Could not resolve type")], + graphQLErrors: [ + { message: "Could not resolve type", path: ["viewer", "book"] }, + ], }) ); } @@ -816,7 +818,7 @@ describe("schema proxy", () => { expect(snapshot.error).toEqual( new ApolloError({ graphQLErrors: [ - new GraphQLError('Expected { foo: "bar" } to be a GraphQL schema.'), + { message: 'Expected { foo: "bar" } to be a GraphQL schema.' }, ], }) ); diff --git a/src/testing/experimental/createSchemaFetch.ts b/src/testing/experimental/createSchemaFetch.ts index 9f018156cf1..c8eb9374759 100644 --- a/src/testing/experimental/createSchemaFetch.ts +++ b/src/testing/experimental/createSchemaFetch.ts @@ -1,6 +1,6 @@ -import { execute, validate } from "graphql"; +import { execute, GraphQLError, validate } from "graphql"; import type { GraphQLFormattedError, GraphQLSchema } from "graphql"; -import { ApolloError, gql } from "../../core/index.js"; +import { gql } from "../../core/index.js"; import { withCleanup } from "../internal/index.js"; import { wait } from "../core/wait.js"; @@ -63,22 +63,15 @@ const createSchemaFetch = ( const document = gql(body.query); if (mockFetchOpts.validate) { - let validationErrors: readonly Error[] = []; + let validationErrors: readonly GraphQLFormattedError[] = []; try { validationErrors = validate(schema, document); } catch (e) { validationErrors = [ - new ApolloError({ - graphQLErrors: [ - /* - * Technically, these are even `GraphQLError` instances, - * but we try to avoid referencing that type, and `GraphQLError` - * implements the `GraphQLFormattedError` interface. - */ - e as GraphQLFormattedError, - ], - }), + e instanceof Error ? + GraphQLError.prototype.toJSON.apply(e) + : (e as any), ]; } diff --git a/src/testing/react/__tests__/MockedProvider.test.tsx b/src/testing/react/__tests__/MockedProvider.test.tsx index 4ebd0878a43..f4a5caed150 100644 --- a/src/testing/react/__tests__/MockedProvider.test.tsx +++ b/src/testing/react/__tests__/MockedProvider.test.tsx @@ -10,6 +10,7 @@ import { InMemoryCache } from "../../../cache"; import { QueryResult } from "../../../react/types/types"; import { ApolloLink, FetchResult } from "../../../link/core"; import { Observable } from "zen-observable-ts"; +import { ApolloError } from "../../../errors"; const variables = { username: "mock_username", @@ -422,7 +423,9 @@ describe("General use", () => { variables, }); if (!loading) { - expect(error).toEqual(new Error("something went wrong")); + expect(error).toEqual( + new ApolloError({ networkError: new Error("something went wrong") }) + ); finished = true; } return null; diff --git a/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap b/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap index 6706568ffd2..e6dfab1ab04 100644 --- a/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap +++ b/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap @@ -8,18 +8,42 @@ Object { `; exports[`General use should error if the query in the mock and component do not match 1`] = ` -[ApolloError: No more mocked responses for the query: query GetUser($username: String!) { +ApolloError { + "cause": [Error: No more mocked responses for the query: query GetUser($username: String!) { user(username: $username) { id __typename } } Expected variables: {"username":"mock_username"} -] +], + "clientErrors": Array [], + "extraInfo": undefined, + "graphQLErrors": Array [], + "message": "No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {\\"username\\":\\"mock_username\\"} +", + "name": "ApolloError", + "networkError": [Error: No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {"username":"mock_username"} +], + "protocolErrors": Array [], +} `; exports[`General use should error if the variableMatcher returns false 1`] = ` -[ApolloError: No more mocked responses for the query: query GetUser($username: String!) { +ApolloError { + "cause": [Error: No more mocked responses for the query: query GetUser($username: String!) { user(username: $username) { id __typename @@ -29,11 +53,40 @@ Expected variables: {"username":"mock_username"} Failed to match 1 mock for this query. The mocked response had the following variables: {} -] +], + "clientErrors": Array [], + "extraInfo": undefined, + "graphQLErrors": Array [], + "message": "No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {\\"username\\":\\"mock_username\\"} + +Failed to match 1 mock for this query. The mocked response had the following variables: + {} +", + "name": "ApolloError", + "networkError": [Error: No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {"username":"mock_username"} + +Failed to match 1 mock for this query. The mocked response had the following variables: + {} +], + "protocolErrors": Array [], +} `; exports[`General use should error if the variables do not deep equal 1`] = ` -[ApolloError: No more mocked responses for the query: query GetUser($username: String!) { +ApolloError { + "cause": [Error: No more mocked responses for the query: query GetUser($username: String!) { user(username: $username) { id __typename @@ -43,11 +96,40 @@ Expected variables: {"username":"some_user","age":42} Failed to match 1 mock for this query. The mocked response had the following variables: {"age":13,"username":"some_user"} -] +], + "clientErrors": Array [], + "extraInfo": undefined, + "graphQLErrors": Array [], + "message": "No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {\\"username\\":\\"some_user\\",\\"age\\":42} + +Failed to match 1 mock for this query. The mocked response had the following variables: + {\\"age\\":13,\\"username\\":\\"some_user\\"} +", + "name": "ApolloError", + "networkError": [Error: No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {"username":"some_user","age":42} + +Failed to match 1 mock for this query. The mocked response had the following variables: + {"age":13,"username":"some_user"} +], + "protocolErrors": Array [], +} `; exports[`General use should error if the variables in the mock and component do not match 1`] = ` -[ApolloError: No more mocked responses for the query: query GetUser($username: String!) { +ApolloError { + "cause": [Error: No more mocked responses for the query: query GetUser($username: String!) { user(username: $username) { id __typename @@ -57,7 +139,35 @@ Expected variables: {"username":"other_user","age":} Failed to match 1 mock for this query. The mocked response had the following variables: {"username":"mock_username"} -] +], + "clientErrors": Array [], + "extraInfo": undefined, + "graphQLErrors": Array [], + "message": "No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {\\"username\\":\\"other_user\\",\\"age\\":} + +Failed to match 1 mock for this query. The mocked response had the following variables: + {\\"username\\":\\"mock_username\\"} +", + "name": "ApolloError", + "networkError": [Error: No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {"username":"other_user","age":} + +Failed to match 1 mock for this query. The mocked response had the following variables: + {"username":"mock_username"} +], + "protocolErrors": Array [], +} `; exports[`General use should mock the data 1`] = ` @@ -76,19 +186,64 @@ Object { } `; -exports[`General use should pipe exceptions thrown in custom onError functions through the link chain 1`] = `[ApolloError: oh no!]`; +exports[`General use should pipe exceptions thrown in custom onError functions through the link chain 1`] = ` +ApolloError { + "cause": [Error: oh no!], + "clientErrors": Array [], + "extraInfo": undefined, + "graphQLErrors": Array [], + "message": "oh no!", + "name": "ApolloError", + "networkError": [Error: oh no!], + "protocolErrors": Array [], +} +`; -exports[`General use should return "Mocked response should contain" errors in response 1`] = `[ApolloError: Mocked response should contain either \`result\`, \`error\` or a \`delay\` of \`Infinity\`: {"query":"query GetUser($username: String!) {\\n user(username: $username) {\\n id\\n __typename\\n }\\n}"}]`; +exports[`General use should return "Mocked response should contain" errors in response 1`] = ` +ApolloError { + "cause": [Error: Mocked response should contain either \`result\`, \`error\` or a \`delay\` of \`Infinity\`: {"query":"query GetUser($username: String!) {\\n user(username: $username) {\\n id\\n __typename\\n }\\n}"}], + "clientErrors": Array [], + "extraInfo": undefined, + "graphQLErrors": Array [], + "message": "Mocked response should contain either \`result\`, \`error\` or a \`delay\` of \`Infinity\`: {\\"query\\":\\"query GetUser($username: String!) {\\\\n user(username: $username) {\\\\n id\\\\n __typename\\\\n }\\\\n}\\"}", + "name": "ApolloError", + "networkError": [Error: Mocked response should contain either \`result\`, \`error\` or a \`delay\` of \`Infinity\`: {"query":"query GetUser($username: String!) {\\n user(username: $username) {\\n id\\n __typename\\n }\\n}"}], + "protocolErrors": Array [], +} +`; exports[`General use should return "No more mocked responses" errors in response 1`] = ` -[ApolloError: No more mocked responses for the query: query GetUser($username: String!) { +ApolloError { + "cause": [Error: No more mocked responses for the query: query GetUser($username: String!) { user(username: $username) { id __typename } } Expected variables: {} -] +], + "clientErrors": Array [], + "extraInfo": undefined, + "graphQLErrors": Array [], + "message": "No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {} +", + "name": "ApolloError", + "networkError": [Error: No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {} +], + "protocolErrors": Array [], +} `; exports[`General use should support custom error handling using setOnError 1`] = ` From 8f3d7eb3bc2e0c2d79c5b1856655abe829390742 Mon Sep 17 00:00:00 2001 From: Sneyder Barreto Date: Mon, 15 Jul 2024 03:02:06 -0500 Subject: [PATCH 56/62] Fix `optimisticResponse` function return type (#11944) * fix(types): explicitly return `IgnoreModifier` from `optimisticResponse` function * Add changeset * Add test that allows returning `IgnoreModifier` when inferring from a generic TypedDocumentNode mutation --------- Co-authored-by: Lenz Weber-Tronic --- .api-reports/api-report-core.api.md | 3 ++- .api-reports/api-report-react.api.md | 3 ++- .../api-report-react_components.api.md | 3 ++- .api-reports/api-report-react_context.api.md | 3 ++- .api-reports/api-report-react_hoc.api.md | 3 ++- .api-reports/api-report-react_hooks.api.md | 3 ++- .api-reports/api-report-react_internal.api.md | 3 ++- .api-reports/api-report-react_ssr.api.md | 3 ++- .api-reports/api-report-testing.api.md | 3 ++- .api-reports/api-report-testing_core.api.md | 3 ++- .api-reports/api-report-utilities.api.md | 3 ++- .api-reports/api-report.api.md | 3 ++- .changeset/pink-flowers-switch.md | 5 +++++ src/__tests__/optimistic.ts | 20 +++++++++++++++++++ src/core/watchQueryOptions.ts | 5 ++++- 15 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 .changeset/pink-flowers-switch.md diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 237eac1a463..e91bf2384b9 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1391,9 +1391,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; update?: MutationUpdaterFunction; updateQueries?: MutationQueryReducersMap; diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index 5b8b33b8ae6..f2acc1ef23f 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1119,9 +1119,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; diff --git a/.api-reports/api-report-react_components.api.md b/.api-reports/api-report-react_components.api.md index 24881bcaf5d..dd2edf25377 100644 --- a/.api-reports/api-report-react_components.api.md +++ b/.api-reports/api-report-react_components.api.md @@ -995,9 +995,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; diff --git a/.api-reports/api-report-react_context.api.md b/.api-reports/api-report-react_context.api.md index fa3fd404a7d..14776931c35 100644 --- a/.api-reports/api-report-react_context.api.md +++ b/.api-reports/api-report-react_context.api.md @@ -978,9 +978,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; diff --git a/.api-reports/api-report-react_hoc.api.md b/.api-reports/api-report-react_hoc.api.md index 6be8fbce071..1e4fe900ffa 100644 --- a/.api-reports/api-report-react_hoc.api.md +++ b/.api-reports/api-report-react_hoc.api.md @@ -997,9 +997,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; diff --git a/.api-reports/api-report-react_hooks.api.md b/.api-reports/api-report-react_hooks.api.md index 0691c35d9ba..beee82502e3 100644 --- a/.api-reports/api-report-react_hooks.api.md +++ b/.api-reports/api-report-react_hooks.api.md @@ -1068,9 +1068,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; diff --git a/.api-reports/api-report-react_internal.api.md b/.api-reports/api-report-react_internal.api.md index 98443e3b6b8..3e85f01b867 100644 --- a/.api-reports/api-report-react_internal.api.md +++ b/.api-reports/api-report-react_internal.api.md @@ -1079,9 +1079,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index eb50183646e..1816993de87 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -963,9 +963,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; diff --git a/.api-reports/api-report-testing.api.md b/.api-reports/api-report-testing.api.md index ebd148cf04e..7019af419eb 100644 --- a/.api-reports/api-report-testing.api.md +++ b/.api-reports/api-report-testing.api.md @@ -1084,9 +1084,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index ef631e90da1..07be1d04650 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1039,9 +1039,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index ab3858c6f0d..2d2108cade6 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -1687,9 +1687,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts update?: MutationUpdaterFunction; diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index ef88362554a..ca7d586070f 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -1572,9 +1572,10 @@ interface MutationBaseOptions; + // Warning: (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier; - }) => TData); + }) => TData | IgnoreModifier); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; update?: MutationUpdaterFunction; updateQueries?: MutationQueryReducersMap; diff --git a/.changeset/pink-flowers-switch.md b/.changeset/pink-flowers-switch.md new file mode 100644 index 00000000000..c85a3e59376 --- /dev/null +++ b/.changeset/pink-flowers-switch.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Allow `IgnoreModifier` to be returned from a `optimisticResponse` function when inferring from a `TypedDocumentNode` when used with a generic argument. diff --git a/src/__tests__/optimistic.ts b/src/__tests__/optimistic.ts index 3cc984868ed..ed53dc8cf9c 100644 --- a/src/__tests__/optimistic.ts +++ b/src/__tests__/optimistic.ts @@ -9,6 +9,7 @@ import { ApolloLink, ApolloCache, MutationQueryReducersMap, + TypedDocumentNode, } from "../core"; import { QueryManager } from "../core/QueryManager"; @@ -1089,6 +1090,25 @@ describe("optimistic mutation results", () => { resolve(); } ); + + it("allows IgnoreModifier as return value when inferring from a TypedDocumentNode mutation", () => { + const mutation: TypedDocumentNode<{ bar: string }> = gql` + mutation foo { + foo { + bar + } + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + }); + + client.mutate({ + mutation, + optimisticResponse: (vars, { IGNORE }) => IGNORE, + }); + }); }); describe("optimistic updates using `updateQueries`", () => { diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index b05cdb13c33..0627f04ebc5 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -220,7 +220,10 @@ export interface MutationBaseOptions< /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#optimisticResponse:member} */ optimisticResponse?: | TData - | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier }) => TData); + | (( + vars: TVariables, + { IGNORE }: { IGNORE: IgnoreModifier } + ) => TData | IgnoreModifier); /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#updateQueries:member} */ updateQueries?: MutationQueryReducersMap; From 0de03af912a76c4e0111f21b4f90a073317b63b6 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 15 Jul 2024 10:06:02 +0200 Subject: [PATCH 57/62] add React 19 RC to `peerDependencies` (#11951) * add React 19 RC to `peerDependencies` * changeset * update react RC dep for tests --- .changeset/breezy-deers-dream.md | 5 +++++ package-lock.json | 35 +++++++++++++++----------------- package.json | 8 ++++---- 3 files changed, 25 insertions(+), 23 deletions(-) create mode 100644 .changeset/breezy-deers-dream.md diff --git a/.changeset/breezy-deers-dream.md b/.changeset/breezy-deers-dream.md new file mode 100644 index 00000000000..71d8b128201 --- /dev/null +++ b/.changeset/breezy-deers-dream.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +add React 19 RC to `peerDependencies` diff --git a/package-lock.json b/package-lock.json index c9065d07452..04309949e03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -84,10 +84,10 @@ "prettier": "3.1.1", "react": "18.3.1", "react-17": "npm:react@^17", - "react-19": "npm:react@19.0.0-rc-fb9a90fa48-20240614", + "react-19": "npm:react@19.0.0-rc-378b305958-20240710", "react-dom": "18.3.1", "react-dom-17": "npm:react-dom@^17", - "react-dom-19": "npm:react-dom@19.0.0-rc-fb9a90fa48-20240614", + "react-dom-19": "npm:react-dom@19.0.0-rc-378b305958-20240710", "react-error-boundary": "4.0.13", "recast": "0.23.9", "resolve": "1.22.8", @@ -116,8 +116,8 @@ "peerDependencies": { "graphql": "^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" }, "peerDependenciesMeta": { @@ -10247,11 +10247,10 @@ }, "node_modules/react-19": { "name": "react", - "version": "19.0.0-rc-fb9a90fa48-20240614", - "resolved": "https://registry.npmjs.org/react/-/react-19.0.0-rc-fb9a90fa48-20240614.tgz", - "integrity": "sha512-nvE3Gy+IOIfH/DXhkyxFVQSrITarFcQz4+shzC/McxQXEUSonpw2oDy/Wi9hdDtV3hlP12VYuDL95iiBREedNQ==", + "version": "19.0.0-rc-378b305958-20240710", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0-rc-378b305958-20240710.tgz", + "integrity": "sha512-z+c5SdYuX74FSlyDcBWXMmR7KN30rpak804OtidcgxvYoyU43YCT0GWHubH6UvnNDvaLsr5nl66AKmC2y7VwYA==", "dev": true, - "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -10296,24 +10295,22 @@ }, "node_modules/react-dom-19": { "name": "react-dom", - "version": "19.0.0-rc-fb9a90fa48-20240614", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-rc-fb9a90fa48-20240614.tgz", - "integrity": "sha512-PoEsPe32F7KPLYOBvZfjylEI1B67N44PwY3lyvpmBkhlluLnLz0jH8q2Wg9YidAi6z0k3iUnNRm5x10wurzt9Q==", + "version": "19.0.0-rc-378b305958-20240710", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0-rc-378b305958-20240710.tgz", + "integrity": "sha512-WpE0+4pnFMyuC9WxJGj7+XysxNChbwnCcFS7INR428/uUoECSTRmlQt05hTGFIyfpYBO1zGlXfMBAkrFmqh3wg==", "dev": true, - "license": "MIT", "dependencies": { - "scheduler": "0.25.0-rc-fb9a90fa48-20240614" + "scheduler": "0.25.0-rc-378b305958-20240710" }, "peerDependencies": { - "react": "19.0.0-rc-fb9a90fa48-20240614" + "react": "19.0.0-rc-378b305958-20240710" } }, "node_modules/react-dom-19/node_modules/scheduler": { - "version": "0.25.0-rc-fb9a90fa48-20240614", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-fb9a90fa48-20240614.tgz", - "integrity": "sha512-HHqQ/SqbeiDfXXVKgNxTpbQTD4n7IUb4hZATvHjp03jr3TF7igehCyHdOjeYTrzIseLO93cTTfSb5f4qWcirMQ==", - "dev": true, - "license": "MIT" + "version": "0.25.0-rc-378b305958-20240710", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0-rc-378b305958-20240710.tgz", + "integrity": "sha512-j6dzH6LeNjhKFSpxUiZT2E8tFvFTCQOn339mmzYA3QBLvIkgAtHzSrpdReKhFzw9x4hxazBjgE/4YwBLQBUNlw==", + "dev": true }, "node_modules/react-error-boundary": { "version": "4.0.13", diff --git a/package.json b/package.json index 6bea971ff0a..09ebdb94fda 100644 --- a/package.json +++ b/package.json @@ -72,8 +72,8 @@ "peerDependencies": { "graphql": "^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" }, "peerDependenciesMeta": { @@ -165,10 +165,10 @@ "prettier": "3.1.1", "react": "18.3.1", "react-17": "npm:react@^17", - "react-19": "npm:react@19.0.0-rc-fb9a90fa48-20240614", + "react-19": "npm:react@19.0.0-rc-378b305958-20240710", "react-dom": "18.3.1", "react-dom-17": "npm:react-dom@^17", - "react-dom-19": "npm:react-dom@19.0.0-rc-fb9a90fa48-20240614", + "react-dom-19": "npm:react-dom@19.0.0-rc-378b305958-20240710", "react-error-boundary": "4.0.13", "recast": "0.23.9", "resolve": "1.22.8", From 4a6e86aeaf6685abf0dd23110784848c8b085735 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 15 Jul 2024 10:27:49 +0200 Subject: [PATCH 58/62] optimize `useQuery` result handling (#11954) * remove unused code path Coverage shows that this code path was never hit - in the case of a `standby` fetchPolicy, `resultData.current` would already have been reset in `useResubscribeIfNecessary` * move code from `toQueryResult` to `setResult` While there are three code paths to `toQueryResult`, the other two are guaranteed to never have an `errors` property * add undocumented `errors` field to types and deprecate it * remove `useHandleSkip` instead, we calculate and memoize an "override result" in `useObservableSubscriptionResult` * remove now-obsolete `originalResult` symbol property * fix typo, chores * adjustment for compiler compat * review feedback * changeset * api-report --- .api-reports/api-report-core.api.md | 3 +- .api-reports/api-report-react.api.md | 5 +- .../api-report-react_components.api.md | 5 +- .api-reports/api-report-react_context.api.md | 5 +- .api-reports/api-report-react_hoc.api.md | 3 +- .api-reports/api-report-react_hooks.api.md | 5 +- .api-reports/api-report-react_internal.api.md | 5 +- .api-reports/api-report-react_ssr.api.md | 5 +- .api-reports/api-report-testing.api.md | 3 +- .api-reports/api-report-testing_core.api.md | 3 +- .api-reports/api-report-utilities.api.md | 3 +- .api-reports/api-report.api.md | 5 +- .changeset/proud-humans-begin.md | 5 + .size-limits.json | 2 +- src/react/hooks/useQuery.ts | 154 +++++++----------- src/react/types/types.ts | 7 +- 16 files changed, 95 insertions(+), 123 deletions(-) create mode 100644 .changeset/proud-humans-begin.md diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index e91bf2384b9..77b976d6673 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -2303,8 +2303,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:275: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 f2acc1ef23f..dc70efc3151 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1695,6 +1695,8 @@ export interface QueryResult; data: TData | undefined; error?: ApolloError; + // @deprecated (undocumented) + errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; @@ -2328,8 +2330,7 @@ interface WatchQueryOptions; data: TData | undefined; error?: ApolloError; + // @deprecated (undocumented) + errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; @@ -1807,8 +1809,7 @@ interface WatchQueryOptions; data: TData | undefined; error?: ApolloError; + // @deprecated (undocumented) + errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; @@ -1727,8 +1729,7 @@ interface WatchQueryOptions; data: TData | undefined; error?: ApolloError; + // @deprecated (undocumented) + errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; @@ -2152,8 +2154,7 @@ interface WatchQueryOptions; data: TData | undefined; error?: ApolloError; + // @deprecated (undocumented) + errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; @@ -2215,8 +2217,7 @@ export function wrapQueryRef(inter // src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:275: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 diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index 1816993de87..4dc802fa4c6 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1412,6 +1412,8 @@ interface QueryResult; data: TData | undefined; error?: ApolloError; + // @deprecated (undocumented) + errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; @@ -1712,8 +1714,7 @@ interface WatchQueryOptions(it: (...args: TArgs // src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index 07be1d04650..1160c018bfe 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1739,8 +1739,7 @@ export function withWarningSpy(it: (...args: TArgs // src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-utilities.api.md b/.api-reports/api-report-utilities.api.md index 2d2108cade6..8754c6c0da2 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -2670,8 +2670,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/utilities/graphql/storeUtils.ts:226:12 - (ae-forgotten-export) The symbol "storeKeyNameStringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index ca7d586070f..7fdbd19f329 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2266,6 +2266,8 @@ export interface QueryResult; data: TData | undefined; error?: ApolloError; + // @deprecated (undocumented) + errors?: ReadonlyArray; loading: boolean; networkStatus: NetworkStatus; observable: ObservableQuery; @@ -3016,8 +3018,7 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:272:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:275: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 diff --git a/.changeset/proud-humans-begin.md b/.changeset/proud-humans-begin.md new file mode 100644 index 00000000000..059cffa65fc --- /dev/null +++ b/.changeset/proud-humans-begin.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Document (and deprecate) the previously undocumented `errors` property on the `useQuery` `QueryResult` type. diff --git a/.size-limits.json b/.size-limits.json index c6da4a36f8f..60d013b1b4f 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 40206, + "dist/apollo-client.min.cjs": 40162, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32999 } diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 8f56c095fe5..5a483f9b4e8 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -61,14 +61,10 @@ const { prototype: { hasOwnProperty }, } = Object; -const originalResult = Symbol(); -interface InternalQueryResult - extends Omit< - QueryResult, - Exclude, "variables"> - > { - [originalResult]: ApolloQueryResult; -} +type InternalQueryResult = Omit< + QueryResult, + Exclude, "variables"> +>; function noop() {} export const lastWatchOptions = Symbol(); @@ -295,22 +291,14 @@ export function useQueryInternals< Omit, "variables"> >(() => bindObservableMethods(observable), [observable]); - useHandleSkip( - resultData, // might get mutated during render - observable, - client, - options, - watchQueryOptions, - disableNetworkFetches, - isSyncSSR - ); - useRegisterSSRObservable(observable, renderPromises, ssrAllowed); const result = useObservableSubscriptionResult( resultData, observable, client, + options, + watchQueryOptions, disableNetworkFetches, partialRefetch, isSyncSSR, @@ -337,9 +325,11 @@ function useObservableSubscriptionResult< resultData: InternalResult, observable: ObservableQuery, client: ApolloClient, + options: QueryHookOptions, NoInfer>, + watchQueryOptions: Readonly>, disableNetworkFetches: boolean, partialRefetch: boolean | undefined, - skipSubscribing: boolean, + isSyncSSR: boolean, callbacks: { onCompleted: (data: TData) => void; onError: (error: ApolloError) => void; @@ -356,6 +346,37 @@ function useObservableSubscriptionResult< callbackRef.current = callbacks; }); + const resultOverride = + ( + (isSyncSSR || disableNetworkFetches) && + options.ssr === false && + !options.skip + ) ? + // If SSR has been explicitly disabled, and this function has been called + // on the server side, return the default loading state. + ssrDisabledResult + : options.skip || watchQueryOptions.fetchPolicy === "standby" ? + // When skipping a query (ie. we're not querying for data but still want to + // render children), make sure the `data` is cleared out and `loading` is + // set to `false` (since we aren't loading anything). + // + // NOTE: We no longer think this is the correct behavior. Skipping should + // not automatically set `data` to `undefined`, but instead leave the + // previous data in place. In other words, skipping should not mandate that + // previously received data is all of a sudden removed. Unfortunately, + // changing this is breaking, so we'll have to wait until Apollo Client 4.0 + // to address this. + skipStandbyResult + : void 0; + + const previousData = resultData.previousData; + const currentResultOverride = React.useMemo( + () => + resultOverride && + toQueryResult(resultOverride, previousData, observable, client), + [client, observable, resultOverride, previousData] + ); + return useSyncExternalStore( React.useCallback( (handleStoreChange) => { @@ -363,7 +384,7 @@ function useObservableSubscriptionResult< // keep it as a dependency of this effect, even though it's not used disableNetworkFetches; - if (skipSubscribing) { + if (isSyncSSR) { return () => {}; } @@ -447,7 +468,7 @@ function useObservableSubscriptionResult< [ disableNetworkFetches, - skipSubscribing, + isSyncSSR, observable, resultData, partialRefetch, @@ -455,6 +476,7 @@ function useObservableSubscriptionResult< ] ), () => + currentResultOverride || getCurrentResult( resultData, observable, @@ -463,6 +485,7 @@ function useObservableSubscriptionResult< client ), () => + currentResultOverride || getCurrentResult( resultData, observable, @@ -488,60 +511,6 @@ function useRegisterSSRObservable( } } -function useHandleSkip< - TData = any, - TVariables extends OperationVariables = OperationVariables, ->( - /** this hook will mutate properties on `resultData` */ - resultData: InternalResult, - observable: ObsQueryWithMeta, - client: ApolloClient, - options: QueryHookOptions, NoInfer>, - watchQueryOptions: Readonly>, - disableNetworkFetches: boolean, - isSyncSSR: boolean -) { - if ( - (isSyncSSR || disableNetworkFetches) && - options.ssr === false && - !options.skip - ) { - // If SSR has been explicitly disabled, and this function has been called - // on the server side, return the default loading state. - resultData.current = toQueryResult( - ssrDisabledResult, - resultData.previousData, - observable, - client - ); - } else if (options.skip || watchQueryOptions.fetchPolicy === "standby") { - // When skipping a query (ie. we're not querying for data but still want to - // render children), make sure the `data` is cleared out and `loading` is - // set to `false` (since we aren't loading anything). - // - // NOTE: We no longer think this is the correct behavior. Skipping should - // not automatically set `data` to `undefined`, but instead leave the - // previous data in place. In other words, skipping should not mandate that - // previously received data is all of a sudden removed. Unfortunately, - // changing this is breaking, so we'll have to wait until Apollo Client 4.0 - // to address this. - resultData.current = toQueryResult( - skipStandbyResult, - resultData.previousData, - observable, - client - ); - } else if ( - // reset result if the last render was skipping for some reason, - // but this render isn't skipping anymore - resultData.current && - (resultData.current[originalResult] === ssrDisabledResult || - resultData.current[originalResult] === skipStandbyResult) - ) { - resultData.current = void 0; - } -} - // this hook is not compatible with any rules of React, and there's no good way to rewrite it. // it should stay a separate hook that will not be optimized by the compiler function useResubscribeIfNecessary< @@ -693,6 +662,15 @@ function setResult( if (previousResult && previousResult.data) { resultData.previousData = previousResult.data; } + + if (!nextResult.error && isNonEmptyArray(nextResult.errors)) { + // Until a set naming convention for networkError and graphQLErrors is + // decided upon, we map errors (graphQLErrors) to the error options. + // TODO: Is it possible for both result.error and result.errors to be + // defined here? + nextResult.error = new ApolloError({ graphQLErrors: nextResult.errors }); + } + resultData.current = toQueryResult( unsafeHandlePartialRefetch(nextResult, observable, partialRefetch), resultData.previousData, @@ -702,16 +680,12 @@ function setResult( // Calling state.setResult always triggers an update, though some call sites // perform additional equality checks before committing to an update. forceUpdate(); - handleErrorOrCompleted( - nextResult, - previousResult?.[originalResult], - callbacks - ); + handleErrorOrCompleted(nextResult, previousResult?.networkStatus, callbacks); } function handleErrorOrCompleted( result: ApolloQueryResult, - previousResult: ApolloQueryResult | undefined, + previousNetworkStatus: NetworkStatus | undefined, callbacks: Callbacks ) { if (!result.loading) { @@ -724,7 +698,7 @@ function handleErrorOrCompleted( callbacks.onError(error); } else if ( result.data && - previousResult?.networkStatus !== result.networkStatus && + previousNetworkStatus !== result.networkStatus && result.networkStatus === NetworkStatus.ready ) { callbacks.onCompleted(result.data); @@ -799,21 +773,7 @@ export function toQueryResult( variables: observable.variables, called: result !== ssrDisabledResult && result !== skipStandbyResult, previousData, - } satisfies Omit< - InternalQueryResult, - typeof originalResult - > as InternalQueryResult; - // non-enumerable property to hold the original result, for referential equality checks - Object.defineProperty(queryResult, originalResult, { value: result }); - - if (!queryResult.error && isNonEmptyArray(result.errors)) { - // Until a set naming convention for networkError and graphQLErrors is - // decided upon, we map errors (graphQLErrors) to the error options. - // TODO: Is it possible for both result.error and result.errors to be - // defined here? - queryResult.error = new ApolloError({ graphQLErrors: result.errors }); - } - + }; return queryResult; } diff --git a/src/react/types/types.ts b/src/react/types/types.ts index cd2e6df1ccb..ab4444596f4 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -1,5 +1,5 @@ import type * as ReactTypes from "react"; -import type { DocumentNode } from "graphql"; +import type { DocumentNode, GraphQLFormattedError } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import type { @@ -148,6 +148,11 @@ export interface QueryResult< previousData?: TData; /** {@inheritDoc @apollo/client!QueryResultDocumentation#error:member} */ error?: ApolloError; + /** + * @deprecated This property will be removed in a future version of Apollo Client. + * Please use `error.graphQLErrors` instead. + */ + errors?: ReadonlyArray; /** {@inheritDoc @apollo/client!QueryResultDocumentation#loading:member} */ loading: boolean; /** {@inheritDoc @apollo/client!QueryResultDocumentation#networkStatus:member} */ From 5da4b2198a51e5c8e6b89e9e2fcc0eabf1f9c152 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 15 Jul 2024 21:55:20 +0200 Subject: [PATCH 59/62] QueryManager: require all options (#11920) --- .api-reports/api-report-core.api.md | 48 ++++--- .api-reports/api-report-react.api.md | 48 ++++--- .../api-report-react_components.api.md | 48 ++++--- .api-reports/api-report-react_context.api.md | 48 ++++--- .api-reports/api-report-react_hoc.api.md | 48 ++++--- .api-reports/api-report-react_hooks.api.md | 48 ++++--- .api-reports/api-report-react_internal.api.md | 48 ++++--- .api-reports/api-report-react_ssr.api.md | 48 ++++--- .api-reports/api-report-testing.api.md | 48 ++++--- .api-reports/api-report-testing_core.api.md | 48 ++++--- .api-reports/api-report-utilities.api.md | 48 ++++--- .api-reports/api-report.api.md | 48 ++++--- .size-limits.json | 4 +- src/__tests__/graphqlSubscriptions.ts | 81 ++++++----- src/core/QueryManager.ts | 63 ++++----- src/core/__tests__/ObservableQuery.ts | 70 ++++++---- src/core/__tests__/QueryManager/index.ts | 130 ++++++++++-------- src/core/__tests__/QueryManager/links.ts | 113 ++++++++------- .../QueryManager/multiple-results.ts | 51 ++++--- src/core/__tests__/QueryManager/recycler.ts | 11 +- src/testing/core/mocking/mockQueryManager.ts | 28 +++- 21 files changed, 701 insertions(+), 426 deletions(-) diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 77b976d6673..6e411da8cfb 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1762,19 +1762,8 @@ export type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -1803,6 +1792,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -1879,6 +1870,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -2300,9 +2317,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/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index dc70efc3151..69378231855 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -1539,19 +1539,8 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -1582,6 +1571,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -1661,6 +1652,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -2325,9 +2342,8 @@ interface WatchQueryOptions void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -1396,6 +1385,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -1475,6 +1466,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -1804,9 +1821,8 @@ interface WatchQueryOptions void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -1324,6 +1313,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -1403,6 +1394,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -1724,9 +1741,8 @@ interface WatchQueryOptions void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -1369,6 +1358,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -1448,6 +1439,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -1751,9 +1768,8 @@ export function withSubscription void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -1451,6 +1440,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -1530,6 +1521,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -2149,9 +2166,8 @@ interface WatchQueryOptions void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -1502,6 +1491,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -1581,6 +1572,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -2212,9 +2229,8 @@ export function wrapQueryRef(inter // 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:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" 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 4dc802fa4c6..dc35a946fb7 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1266,19 +1266,8 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -1309,6 +1298,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -1388,6 +1379,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -1709,9 +1726,8 @@ interface WatchQueryOptions void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -1390,6 +1379,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -1469,6 +1460,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -1777,9 +1794,8 @@ export function withWarningSpy(it: (...args: TArgs // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" 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 1160c018bfe..3bc699063cb 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1304,19 +1304,8 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -1347,6 +1336,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -1426,6 +1417,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -1734,9 +1751,8 @@ export function withWarningSpy(it: (...args: TArgs // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" 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 8754c6c0da2..8788d7cf02d 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -2068,19 +2068,8 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -2111,6 +2100,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -2190,6 +2181,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -2665,9 +2682,8 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts // src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:275:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" 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 7fdbd19f329..69307a44778 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -2113,19 +2113,8 @@ export type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }); + // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts + constructor(options: QueryManagerOptions); // (undocumented) readonly assumeImmutableResults: boolean; // (undocumented) @@ -2154,6 +2143,8 @@ class QueryManager { // // (undocumented) getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // Warning: (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocalState(): LocalState; // (undocumented) @@ -2230,6 +2221,32 @@ class QueryManager { watchQuery(options: WatchQueryOptions): ObservableQuery; } +// @public (undocumented) +interface QueryManagerOptions { + // (undocumented) + assumeImmutableResults: boolean; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clientAwareness: Record; + // (undocumented) + defaultContext: Partial | undefined; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + documentTransform: DocumentTransform | null | undefined; + // (undocumented) + link: ApolloLink; + // (undocumented) + localState: LocalState; + // (undocumented) + onBroadcast: undefined | (() => void); + // (undocumented) + queryDeduplication: boolean; + // (undocumented) + ssrMode: boolean; +} + // @public interface QueryOptions { // @deprecated @@ -3015,9 +3032,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/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts // src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:391:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:138:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:382:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:275: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 diff --git a/.size-limits.json b/.size-limits.json index 60d013b1b4f..3b2a83ce94c 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 40162, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32999 + "dist/apollo-client.min.cjs": 40179, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32990 } diff --git a/src/__tests__/graphqlSubscriptions.ts b/src/__tests__/graphqlSubscriptions.ts index 2f4d66228d9..d666ed22e65 100644 --- a/src/__tests__/graphqlSubscriptions.ts +++ b/src/__tests__/graphqlSubscriptions.ts @@ -7,6 +7,7 @@ import { QueryManager } from "../core/QueryManager"; import { itAsync, mockObservableLink } from "../testing"; import { GraphQLError } from "graphql"; import { spyOnConsole } from "../testing/internal"; +import { getDefaultOptionsForQueryManagerTests } from "../testing/core/mocking/mockQueryManager"; describe("GraphQL Subscriptions", () => { const results = [ @@ -103,10 +104,12 @@ describe("GraphQL Subscriptions", () => { itAsync("should multiplex subscriptions", (resolve, reject) => { const link = mockObservableLink(); - const queryManager = new QueryManager({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache({ addTypename: false }), + }) + ); const obs = queryManager.startGraphQLSubscription(options); @@ -143,10 +146,12 @@ describe("GraphQL Subscriptions", () => { (resolve, reject) => { const link = mockObservableLink(); let numResults = 0; - const queryManager = new QueryManager({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache({ addTypename: false }), + }) + ); // tslint:disable-next-line queryManager.startGraphQLSubscription(options).subscribe({ @@ -192,10 +197,12 @@ describe("GraphQL Subscriptions", () => { it("should throw an error if the result has errors on it", () => { const link = mockObservableLink(); - const queryManager = new QueryManager({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache({ addTypename: false }), + }) + ); const obs = queryManager.startGraphQLSubscription(options); @@ -242,10 +249,12 @@ describe("GraphQL Subscriptions", () => { } `; const link = mockObservableLink(); - const queryManager = new QueryManager({ - link, - cache: new InMemoryCache(), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache(), + }) + ); const obs = queryManager.startGraphQLSubscription({ query, @@ -289,10 +298,12 @@ describe("GraphQL Subscriptions", () => { } `; const link = mockObservableLink(); - const queryManager = new QueryManager({ - link, - cache: new InMemoryCache(), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache(), + }) + ); const obs = queryManager.startGraphQLSubscription({ query, @@ -358,10 +369,12 @@ describe("GraphQL Subscriptions", () => { } `; const link = mockObservableLink(); - const queryManager = new QueryManager({ - link, - cache: new InMemoryCache(), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache(), + }) + ); const obs = queryManager.startGraphQLSubscription({ query, @@ -400,10 +413,12 @@ describe("GraphQL Subscriptions", () => { } `; const link = mockObservableLink(); - const queryManager = new QueryManager({ - link, - cache: new InMemoryCache(), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache(), + }) + ); const obs = queryManager.startGraphQLSubscription({ query, @@ -501,10 +516,12 @@ describe("GraphQL Subscriptions", () => { it("should throw an error if the result has protocolErrors on it", async () => { const link = mockObservableLink(); - const queryManager = new QueryManager({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache({ addTypename: false }), + }) + ); const obs = queryManager.startGraphQLSubscription(options); diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 9d230fcc3c0..6ba645285c8 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -64,7 +64,7 @@ import type { InternalRefetchQueriesMap, DefaultContext, } from "./types.js"; -import { LocalState } from "./LocalState.js"; +import type { LocalState } from "./LocalState.js"; import type { QueryStoreValue } from "./QueryInfo.js"; import { @@ -105,6 +105,20 @@ import type { DefaultOptions } from "./ApolloClient.js"; import { Trie } from "@wry/trie"; import { AutoCleanedWeakCache, cacheSizes } from "../utilities/index.js"; +export interface QueryManagerOptions { + cache: ApolloCache; + link: ApolloLink; + defaultOptions: DefaultOptions; + documentTransform: DocumentTransform | null | undefined; + queryDeduplication: boolean; + onBroadcast: undefined | (() => void); + ssrMode: boolean; + clientAwareness: Record; + localState: LocalState; + assumeImmutableResults: boolean; + defaultContext: Partial | undefined; +} + export class QueryManager { public cache: ApolloCache; public link: ApolloLink; @@ -134,45 +148,22 @@ export class QueryManager { // @apollo/experimental-nextjs-app-support can access type info. protected fetchCancelFns = new Map any>(); - constructor({ - cache, - link, - defaultOptions, - documentTransform, - queryDeduplication = false, - onBroadcast, - ssrMode = false, - clientAwareness = {}, - localState, - assumeImmutableResults = !!cache.assumeImmutableResults, - defaultContext, - }: { - cache: ApolloCache; - link: ApolloLink; - defaultOptions?: DefaultOptions; - documentTransform?: DocumentTransform; - queryDeduplication?: boolean; - onBroadcast?: () => void; - ssrMode?: boolean; - clientAwareness?: Record; - localState?: LocalState; - assumeImmutableResults?: boolean; - defaultContext?: Partial; - }) { + constructor(options: QueryManagerOptions) { const defaultDocumentTransform = new DocumentTransform( (document) => this.cache.transformDocument(document), // Allow the apollo cache to manage its own transform caches { cache: false } ); - this.cache = cache; - this.link = link; - this.defaultOptions = defaultOptions || Object.create(null); - this.queryDeduplication = queryDeduplication; - this.clientAwareness = clientAwareness; - this.localState = localState || new LocalState({ cache }); - this.ssrMode = ssrMode; - this.assumeImmutableResults = assumeImmutableResults; + this.cache = options.cache; + this.link = options.link; + this.defaultOptions = options.defaultOptions; + this.queryDeduplication = options.queryDeduplication; + this.clientAwareness = options.clientAwareness; + this.localState = options.localState; + this.ssrMode = options.ssrMode; + this.assumeImmutableResults = options.assumeImmutableResults; + const documentTransform = options.documentTransform; this.documentTransform = documentTransform ? defaultDocumentTransform @@ -183,9 +174,9 @@ export class QueryManager { // selections and fragments from the fragment registry. .concat(defaultDocumentTransform) : defaultDocumentTransform; - this.defaultContext = defaultContext || Object.create(null); + this.defaultContext = options.defaultContext || Object.create(null); - if ((this.onBroadcast = onBroadcast)) { + if ((this.onBroadcast = options.onBroadcast)) { this.mutationStore = Object.create(null); } } diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index b103fb38915..3b639e14b58 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -28,7 +28,9 @@ import { subscribeAndCount, wait, } from "../../testing"; -import mockQueryManager from "../../testing/core/mocking/mockQueryManager"; +import mockQueryManager, { + getDefaultOptionsForQueryManagerTests, +} from "../../testing/core/mocking/mockQueryManager"; import mockWatchQuery from "../../testing/core/mocking/mockWatchQuery"; import wrap from "../../testing/core/wrap"; @@ -97,13 +99,15 @@ describe("ObservableQuery", () => { }); const createQueryManager = ({ link }: { link: ApolloLink }) => { - return new QueryManager({ - link, - assumeImmutableResults: true, - cache: new InMemoryCache({ - addTypename: false, - }), - }); + return new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + assumeImmutableResults: true, + cache: new InMemoryCache({ + addTypename: false, + }), + }) + ); }; describe("setOptions", () => { @@ -1093,14 +1097,16 @@ describe("ObservableQuery", () => { it("calling refetch with different variables before the query itself resolved will only yield the result for the new variables", async () => { const observers: SubscriptionObserver>[] = []; - const queryManager = new QueryManager({ - cache: new InMemoryCache(), - link: new ApolloLink((operation, forward) => { - return new Observable((observer) => { - observers.push(observer); - }); - }), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache(), + link: new ApolloLink((operation, forward) => { + return new Observable((observer) => { + observers.push(observer); + }); + }), + }) + ); const observableQuery = queryManager.watchQuery({ query, variables: { id: 1 }, @@ -1128,14 +1134,16 @@ describe("ObservableQuery", () => { it("calling refetch multiple times with different variables will return only results for the most recent variables", async () => { const observers: SubscriptionObserver>[] = []; - const queryManager = new QueryManager({ - cache: new InMemoryCache(), - link: new ApolloLink((operation, forward) => { - return new Observable((observer) => { - observers.push(observer); - }); - }), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache(), + link: new ApolloLink((operation, forward) => { + return new Observable((observer) => { + observers.push(observer); + }); + }), + }) + ); const observableQuery = queryManager.watchQuery({ query, variables: { id: 1 }, @@ -1700,10 +1708,12 @@ describe("ObservableQuery", () => { // manually to be able to turn off warnings for this test. const mocks = [makeMock("a", "b", "c"), makeMock("d", "e")]; const firstRequest = mocks[0].request; - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link: new MockLink(mocks, true, { showWarnings: false }), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink(mocks, true, { showWarnings: false }), + }) + ); const observableWithVarsVar = queryManager.watchQuery({ query: firstRequest.query, @@ -2741,7 +2751,9 @@ describe("ObservableQuery", () => { const cache = new InMemoryCache({}); cache.writeQuery({ query, data: cacheValues.initial }); - const queryManager = new QueryManager({ link, cache }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ link, cache }) + ); const observableQuery = queryManager.watchQuery({ query, fetchPolicy, diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index 49068a77548..e0bda61a9a5 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -18,7 +18,9 @@ import { } from "../../../cache/inmemory/types"; // mocks -import mockQueryManager from "../../../testing/core/mocking/mockQueryManager"; +import mockQueryManager, { + getDefaultOptionsForQueryManagerTests, +} from "../../../testing/core/mocking/mockQueryManager"; import mockWatchQuery from "../../../testing/core/mocking/mockWatchQuery"; import { MockApolloLink, @@ -89,14 +91,16 @@ describe("QueryManager", () => { clientAwareness?: { [key: string]: string }; queryDeduplication?: boolean; }) => { - return new QueryManager({ - link, - cache: new InMemoryCache({ addTypename: false, ...config }), - clientAwareness, - queryDeduplication, - // Enable client.queryManager.mutationStore tracking. - onBroadcast() {}, - }); + return new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache({ addTypename: false, ...config }), + clientAwareness, + queryDeduplication, + // Enable client.queryManager.mutationStore tracking. + onBroadcast() {}, + }) + ); }; // Helper method that sets up a mockQueryManager and then passes on the @@ -539,10 +543,12 @@ describe("QueryManager", () => { }); }); - const mockedQueryManger = new QueryManager({ - link: mockedSingleLink, - cache: new InMemoryCache({ addTypename: false }), - }); + const mockedQueryManger = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link: mockedSingleLink, + cache: new InMemoryCache({ addTypename: false }), + }) + ); const observableQuery = mockedQueryManger.watchQuery({ query: request.query, @@ -620,22 +626,24 @@ describe("QueryManager", () => { }); }); - const mockedQueryManger = new QueryManager({ - link: mockedSingleLink, - cache: new InMemoryCache({ addTypename: false }), - defaultOptions: { - watchQuery: { - fetchPolicy: "cache-and-network", - returnPartialData: false, - partialRefetch: true, - notifyOnNetworkStatusChange: true, - }, - query: { - fetchPolicy: "network-only", + const mockedQueryManger = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link: mockedSingleLink, + cache: new InMemoryCache({ addTypename: false }), + defaultOptions: { + watchQuery: { + fetchPolicy: "cache-and-network", + returnPartialData: false, + partialRefetch: true, + notifyOnNetworkStatusChange: true, + }, + query: { + fetchPolicy: "network-only", + }, }, - }, - queryDeduplication: false, - }); + queryDeduplication: false, + }) + ); const observableQuery = mockedQueryManger.watchQuery< (typeof expResult)["data"], @@ -2893,14 +2901,16 @@ describe("QueryManager", () => { age: "32", }, }; - const queryManager = new QueryManager({ - link: mockSingleLink( - { request: { query: queryA }, result: { data: dataA } }, - { request: { query: queryB }, result: { data: dataB }, delay: 20 } - ).setOnError(reject), - cache: new InMemoryCache({}), - ssrMode: true, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link: mockSingleLink( + { request: { query: queryA }, result: { data: dataA } }, + { request: { query: queryB }, result: { data: dataB }, delay: 20 } + ).setOnError(reject), + cache: new InMemoryCache({}), + ssrMode: true, + }) + ); const observableA = queryManager.watchQuery({ query: queryA, @@ -3070,24 +3080,26 @@ describe("QueryManager", () => { }, }; - const queryManager = new QueryManager({ - link: mockSingleLink( - { - request: { query, variables }, - result: { data: data1 }, - }, - { - request: { query, variables }, - result: { data: data2 }, - }, - { - request: { query, variables }, - result: { data: data2 }, - } - ).setOnError(reject), - cache: new InMemoryCache({ addTypename: false }), - ssrMode: true, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link: mockSingleLink( + { + request: { query, variables }, + result: { data: data1 }, + }, + { + request: { query, variables }, + result: { data: data2 }, + }, + { + request: { query, variables }, + result: { data: data2 }, + } + ).setOnError(reject), + cache: new InMemoryCache({ addTypename: false }), + ssrMode: true, + }) + ); const observable = queryManager.watchQuery({ query, @@ -5984,10 +5996,12 @@ describe("QueryManager", () => { ).setOnError(reject); const cache = new InMemoryCache(); - const queryManager = new QueryManager({ - link, - cache, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache, + }) + ); return queryManager .query({ query: query1 }) diff --git a/src/core/__tests__/QueryManager/links.ts b/src/core/__tests__/QueryManager/links.ts index 7719a8f201f..53d3b22f6bb 100644 --- a/src/core/__tests__/QueryManager/links.ts +++ b/src/core/__tests__/QueryManager/links.ts @@ -15,6 +15,7 @@ import { itAsync, MockSubscriptionLink } from "../../../testing/core"; // core import { QueryManager } from "../../QueryManager"; import { NextLink, Operation, Reference } from "../../../core"; +import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; describe("Link interactions", () => { itAsync( @@ -56,10 +57,12 @@ describe("Link interactions", () => { const mockLink = new MockSubscriptionLink(); const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); const observable = queryManager.watchQuery({ query, @@ -102,10 +105,12 @@ describe("Link interactions", () => { }; const link = new MockSubscriptionLink(); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); const observable = queryManager.watchQuery({ query, @@ -176,10 +181,12 @@ describe("Link interactions", () => { }; const link = new MockSubscriptionLink(); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); const observable = queryManager.watchQuery({ query, @@ -256,10 +263,12 @@ describe("Link interactions", () => { const mockLink = new MockSubscriptionLink(); const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); queryManager.mutate({ mutation }); @@ -298,10 +307,12 @@ describe("Link interactions", () => { const mockLink = new MockSubscriptionLink(); const link = ApolloLink.from([evictionLink, mockLink]); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); queryManager.mutate({ mutation, context: { planet: "Tatooine" } }); @@ -341,34 +352,36 @@ describe("Link interactions", () => { return Observable.of({ data: bookData }); }); - const queryManager = new QueryManager({ - link, - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - book(_, { args, toReference, readField }) { - if (!args) { - throw new Error("arg must never be null"); - } - - const ref = toReference({ __typename: "Book", id: args.id }); - if (!ref) { - throw new Error("ref must never be null"); - } - - expect(ref).toEqual({ __ref: `Book:${args.id}` }); - const found = readField("books")!.find( - (book) => book.__ref === ref.__ref - ); - expect(found).toBeTruthy(); - return found; + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + book(_, { args, toReference, readField }) { + if (!args) { + throw new Error("arg must never be null"); + } + + const ref = toReference({ __typename: "Book", id: args.id }); + if (!ref) { + throw new Error("ref must never be null"); + } + + expect(ref).toEqual({ __ref: `Book:${args.id}` }); + const found = readField("books")!.find( + (book) => book.__ref === ref.__ref + ); + expect(found).toBeTruthy(); + return found; + }, }, }, }, - }, - }), - }); + }), + }) + ); await queryManager.query({ query }); @@ -418,10 +431,12 @@ describe("Link interactions", () => { }); }); - const queryManager = new QueryManager({ - link, - cache: new InMemoryCache({ addTypename: false }), - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link, + cache: new InMemoryCache({ addTypename: false }), + }) + ); await queryManager.query({ query }); diff --git a/src/core/__tests__/QueryManager/multiple-results.ts b/src/core/__tests__/QueryManager/multiple-results.ts index b72f382dca5..1d49bbb770b 100644 --- a/src/core/__tests__/QueryManager/multiple-results.ts +++ b/src/core/__tests__/QueryManager/multiple-results.ts @@ -8,6 +8,7 @@ import { itAsync, MockSubscriptionLink } from "../../../testing/core"; // core import { QueryManager } from "../../QueryManager"; import { GraphQLError } from "graphql"; +import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; describe("mutiple results", () => { itAsync("allows multiple query results from link", (resolve, reject) => { @@ -37,10 +38,12 @@ describe("mutiple results", () => { }, }; const link = new MockSubscriptionLink(); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); const observable = queryManager.watchQuery({ query, @@ -96,10 +99,12 @@ describe("mutiple results", () => { }, }; const link = new MockSubscriptionLink(); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); const observable = queryManager.watchQuery({ query, @@ -166,10 +171,12 @@ describe("mutiple results", () => { }, }; const link = new MockSubscriptionLink(); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); const observable = queryManager.watchQuery({ query, @@ -242,10 +249,12 @@ describe("mutiple results", () => { }, }; const link = new MockSubscriptionLink(); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); const observable = queryManager.watchQuery({ query, @@ -313,10 +322,12 @@ describe("mutiple results", () => { }; const link = new MockSubscriptionLink(); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); const observable = queryManager.watchQuery({ query, diff --git a/src/core/__tests__/QueryManager/recycler.ts b/src/core/__tests__/QueryManager/recycler.ts index e650d0a126e..fccddc901de 100644 --- a/src/core/__tests__/QueryManager/recycler.ts +++ b/src/core/__tests__/QueryManager/recycler.ts @@ -16,6 +16,7 @@ import { InMemoryCache } from "../../../cache"; // mocks import { MockSubscriptionLink } from "../../../testing/core"; +import { getDefaultOptionsForQueryManagerTests } from "../../../testing/core/mocking/mockQueryManager"; describe("Subscription lifecycles", () => { itAsync( @@ -40,10 +41,12 @@ describe("Subscription lifecycles", () => { }; const link = new MockSubscriptionLink(); - const queryManager = new QueryManager({ - cache: new InMemoryCache({ addTypename: false }), - link, - }); + const queryManager = new QueryManager( + getDefaultOptionsForQueryManagerTests({ + cache: new InMemoryCache({ addTypename: false }), + link, + }) + ); // step 1, get some data const observable = queryManager.watchQuery({ diff --git a/src/testing/core/mocking/mockQueryManager.ts b/src/testing/core/mocking/mockQueryManager.ts index a4ded3dfbc5..6be4ab764ed 100644 --- a/src/testing/core/mocking/mockQueryManager.ts +++ b/src/testing/core/mocking/mockQueryManager.ts @@ -1,13 +1,33 @@ +import type { QueryManagerOptions } from "../../../core/QueryManager.js"; import { QueryManager } from "../../../core/QueryManager.js"; import type { MockedResponse } from "./mockLink.js"; import { mockSingleLink } from "./mockLink.js"; import { InMemoryCache } from "../../../cache/index.js"; +import { LocalState } from "../../../core/LocalState.js"; + +export const getDefaultOptionsForQueryManagerTests = ( + options: Pick, "cache" | "link"> & + Partial> +) => ({ + defaultOptions: Object.create(null), + documentTransform: undefined, + queryDeduplication: false, + onBroadcast: undefined, + ssrMode: false, + clientAwareness: {}, + localState: new LocalState({ cache: options.cache }), + assumeImmutableResults: !!options.cache.assumeImmutableResults, + defaultContext: undefined, + ...options, +}); // Helper method for the tests that construct a query manager out of a // a list of mocked responses for a mocked network interface. export default (...mockedResponses: MockedResponse[]) => { - return new QueryManager({ - link: mockSingleLink(...mockedResponses), - cache: new InMemoryCache({ addTypename: false }), - }); + return new QueryManager( + getDefaultOptionsForQueryManagerTests({ + link: mockSingleLink(...mockedResponses), + cache: new InMemoryCache({ addTypename: false }), + }) + ); }; From 4e2a0bb93272e93a6e80310258a94f85f98b0f30 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:03:24 -0600 Subject: [PATCH 60/62] Version Packages (rc) (#11952) Co-authored-by: github-actions[bot] --- .changeset/pre.json | 4 ++++ CHANGELOG.md | 12 ++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.changeset/pre.json b/.changeset/pre.json index 2f8559cf695..2cb29184749 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -7,9 +7,11 @@ "changesets": [ "angry-ravens-mate", "angry-seals-jog", + "breezy-deers-dream", "chilly-dots-shake", "clever-bikes-admire", "curly-vans-draw", + "early-tips-vanish", "flat-onions-guess", "fluffy-badgers-rush", "good-suns-happen", @@ -17,6 +19,8 @@ "little-suits-return", "nasty-olives-act", "pink-ants-remember", + "pink-flowers-switch", + "proud-humans-begin", "slimy-balloons-cheat", "slimy-berries-yawn", "tasty-chairs-dress", diff --git a/CHANGELOG.md b/CHANGELOG.md index 03ebf678445..c3ecbd0181c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # @apollo/client +## 3.11.0-rc.2 + +### Patch Changes + +- [#11951](https://github.com/apollographql/apollo-client/pull/11951) [`0de03af`](https://github.com/apollographql/apollo-client/commit/0de03af912a76c4e0111f21b4f90a073317b63b6) Thanks [@phryneas](https://github.com/phryneas)! - add React 19 RC to `peerDependencies` + +- [#11937](https://github.com/apollographql/apollo-client/pull/11937) [`78332be`](https://github.com/apollographql/apollo-client/commit/78332be32a9af0da33eb3e4100e7a76c3eac2496) Thanks [@phryneas](https://github.com/phryneas)! - `createSchemaFetch`: simulate serialized errors instead of an `ApolloError` instance + +- [#11944](https://github.com/apollographql/apollo-client/pull/11944) [`8f3d7eb`](https://github.com/apollographql/apollo-client/commit/8f3d7eb3bc2e0c2d79c5b1856655abe829390742) Thanks [@sneyderdev](https://github.com/sneyderdev)! - Allow `IgnoreModifier` to be returned from a `optimisticResponse` function when inferring from a `TypedDocumentNode` when used with a generic argument. + +- [#11954](https://github.com/apollographql/apollo-client/pull/11954) [`4a6e86a`](https://github.com/apollographql/apollo-client/commit/4a6e86aeaf6685abf0dd23110784848c8b085735) Thanks [@phryneas](https://github.com/phryneas)! - Document (and deprecate) the previously undocumented `errors` property on the `useQuery` `QueryResult` type. + ## 3.11.0-rc.1 ### Patch Changes diff --git a/package-lock.json b/package-lock.json index 04309949e03..fd75e88174b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.11.0-rc.1", + "version": "3.11.0-rc.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.11.0-rc.1", + "version": "3.11.0-rc.2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 09ebdb94fda..f48e40e57d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.11.0-rc.1", + "version": "3.11.0-rc.2", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ From 56f1c9359dacbd65ff7a8beb2e0af09afb68f35d Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 19 Jul 2024 18:05:41 +0200 Subject: [PATCH 61/62] patch React 19 for tests to make FALLBACK_THROTTLE_MS configurable (#11963) --- ...dom-19+19.0.0-rc-378b305958-20240710.patch | 88 +++++++++++++++++++ src/config/jest/setup.ts | 3 + 2 files changed, 91 insertions(+) create mode 100644 patches/react-dom-19+19.0.0-rc-378b305958-20240710.patch diff --git a/patches/react-dom-19+19.0.0-rc-378b305958-20240710.patch b/patches/react-dom-19+19.0.0-rc-378b305958-20240710.patch new file mode 100644 index 00000000000..fc7597f276a --- /dev/null +++ b/patches/react-dom-19+19.0.0-rc-378b305958-20240710.patch @@ -0,0 +1,88 @@ +diff --git a/node_modules/react-dom-19/cjs/react-dom-client.development.js b/node_modules/react-dom-19/cjs/react-dom-client.development.js +index f9ae214..c29f983 100644 +--- a/node_modules/react-dom-19/cjs/react-dom-client.development.js ++++ b/node_modules/react-dom-19/cjs/react-dom-client.development.js +@@ -14401,7 +14401,7 @@ + (lanes & RetryLanes) === lanes && + ((didTimeout = + globalMostRecentFallbackTime + +- FALLBACK_THROTTLE_MS - ++ (globalThis.REACT_FALLBACK_THROTTLE_MS || FALLBACK_THROTTLE_MS) - + now$1()), + 10 < didTimeout) + ) { +@@ -15599,7 +15599,7 @@ + (workInProgressRootExitStatus === RootSuspended && + (workInProgressRootRenderLanes & RetryLanes) === + workInProgressRootRenderLanes && +- now$1() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS) ++ now$1() - globalMostRecentFallbackTime < (globalThis.REACT_FALLBACK_THROTTLE_MS || FALLBACK_THROTTLE_MS)) + ? (executionContext & RenderContext) === NoContext && + prepareFreshStack(root, 0) + : (workInProgressRootPingedLanes |= pingedLanes)); +diff --git a/node_modules/react-dom-19/cjs/react-dom-client.production.js b/node_modules/react-dom-19/cjs/react-dom-client.production.js +index b93642c..66bb184 100644 +--- a/node_modules/react-dom-19/cjs/react-dom-client.production.js ++++ b/node_modules/react-dom-19/cjs/react-dom-client.production.js +@@ -10071,7 +10071,7 @@ function performConcurrentWorkOnRoot(root, didTimeout) { + } + if ( + (lanes & 62914560) === lanes && +- ((didTimeout = globalMostRecentFallbackTime + 300 - now()), ++ ((didTimeout = globalMostRecentFallbackTime + (globalThis.REACT_FALLBACK_THROTTLE_MS || 300) - now()), + 10 < didTimeout) + ) { + markRootSuspended( +@@ -10936,7 +10936,7 @@ function pingSuspendedRoot(root, wakeable, pingedLanes) { + (3 === workInProgressRootExitStatus && + (workInProgressRootRenderLanes & 62914560) === + workInProgressRootRenderLanes && +- 300 > now() - globalMostRecentFallbackTime) ++ (globalThis.REACT_FALLBACK_THROTTLE_MS || 300) > now() - globalMostRecentFallbackTime) + ? 0 === (executionContext & 2) && prepareFreshStack(root, 0) + : (workInProgressRootPingedLanes |= pingedLanes)); + ensureRootIsScheduled(root); +diff --git a/node_modules/react-dom-19/cjs/react-dom-profiling.development.js b/node_modules/react-dom-19/cjs/react-dom-profiling.development.js +index 9e4fe6a..a4bd41e 100644 +--- a/node_modules/react-dom-19/cjs/react-dom-profiling.development.js ++++ b/node_modules/react-dom-19/cjs/react-dom-profiling.development.js +@@ -14409,7 +14409,7 @@ + (lanes & RetryLanes) === lanes && + ((didTimeout = + globalMostRecentFallbackTime + +- FALLBACK_THROTTLE_MS - ++ (globalThis.REACT_FALLBACK_THROTTLE_MS || FALLBACK_THROTTLE_MS) - + now$1()), + 10 < didTimeout) + ) { +@@ -15611,7 +15611,7 @@ + (workInProgressRootExitStatus === RootSuspended && + (workInProgressRootRenderLanes & RetryLanes) === + workInProgressRootRenderLanes && +- now$1() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS) ++ now$1() - globalMostRecentFallbackTime < (globalThis.REACT_FALLBACK_THROTTLE_MS || FALLBACK_THROTTLE_MS)) + ? (executionContext & RenderContext) === NoContext && + prepareFreshStack(root, 0) + : (workInProgressRootPingedLanes |= pingedLanes)); +diff --git a/node_modules/react-dom-19/cjs/react-dom-profiling.profiling.js b/node_modules/react-dom-19/cjs/react-dom-profiling.profiling.js +index 683c9d9..000bb78 100644 +--- a/node_modules/react-dom-19/cjs/react-dom-profiling.profiling.js ++++ b/node_modules/react-dom-19/cjs/react-dom-profiling.profiling.js +@@ -10600,7 +10600,7 @@ function performConcurrentWorkOnRoot(root, didTimeout) { + } + if ( + (lanes & 62914560) === lanes && +- ((didTimeout = globalMostRecentFallbackTime + 300 - now$1()), ++ ((didTimeout = globalMostRecentFallbackTime + (globalThis.REACT_FALLBACK_THROTTLE_MS || 300) - now$1()), + 10 < didTimeout) + ) { + markRootSuspended( +@@ -11621,7 +11621,7 @@ function pingSuspendedRoot(root, wakeable, pingedLanes) { + (3 === workInProgressRootExitStatus && + (workInProgressRootRenderLanes & 62914560) === + workInProgressRootRenderLanes && +- 300 > now$1() - globalMostRecentFallbackTime) ++ (globalThis.REACT_FALLBACK_THROTTLE_MS || 300) > now$1() - globalMostRecentFallbackTime) + ? 0 === (executionContext & 2) && prepareFreshStack(root, 0) + : (workInProgressRootPingedLanes |= pingedLanes)); + ensureRootIsScheduled(root); diff --git a/src/config/jest/setup.ts b/src/config/jest/setup.ts index f5b6a3e2496..e6bbe2c43c0 100644 --- a/src/config/jest/setup.ts +++ b/src/config/jest/setup.ts @@ -32,3 +32,6 @@ if (!Symbol.asyncDispose) { // @ts-ignore expect.addEqualityTesters([areApolloErrorsEqual, areGraphQLErrorsEqual]); + +// @ts-ignore +globalThis.REACT_FALLBACK_THROTTLE_MS = 10; From f7aed4ff873ac70fa9192f1cfbee013d1800a2bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 22 Jul 2024 23:14:23 +0000 Subject: [PATCH 62/62] Exit prerelease mode --- .changeset/pre.json | 31 ------------------------------- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 3 insertions(+), 34 deletions(-) delete mode 100644 .changeset/pre.json diff --git a/.changeset/pre.json b/.changeset/pre.json deleted file mode 100644 index 2cb29184749..00000000000 --- a/.changeset/pre.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "mode": "pre", - "tag": "rc", - "initialVersions": { - "@apollo/client": "3.10.8" - }, - "changesets": [ - "angry-ravens-mate", - "angry-seals-jog", - "breezy-deers-dream", - "chilly-dots-shake", - "clever-bikes-admire", - "curly-vans-draw", - "early-tips-vanish", - "flat-onions-guess", - "fluffy-badgers-rush", - "good-suns-happen", - "hungry-rings-help", - "little-suits-return", - "nasty-olives-act", - "pink-ants-remember", - "pink-flowers-switch", - "proud-humans-begin", - "slimy-balloons-cheat", - "slimy-berries-yawn", - "tasty-chairs-dress", - "thin-lies-begin", - "unlucky-birds-press", - "weak-ads-develop" - ] -} diff --git a/package-lock.json b/package-lock.json index fd75e88174b..e6eaef73bac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.11.0-rc.2", + "version": "3.10.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.11.0-rc.2", + "version": "3.10.8", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index f48e40e57d1..6dc0c086f3a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.11.0-rc.2", + "version": "3.10.8", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [