diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index f57dfc0b93d..343bb599d38 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -933,7 +933,7 @@ export interface MockedResponse, TVariables = Record // (undocumented) maxUsageCount?: number; // (undocumented) - newData?: ResultFunction; + newData?: ResultFunction, TVariables>; // (undocumented) request: GraphQLRequest; // (undocumented) diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index bce86dc8645..c8121f60b5b 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -888,7 +888,7 @@ export interface MockedResponse, TVariables = Record // (undocumented) maxUsageCount?: number; // (undocumented) - newData?: ResultFunction; + newData?: ResultFunction, TVariables>; // (undocumented) request: GraphQLRequest; // (undocumented) diff --git a/.changeset/tiny-vans-draw.md b/.changeset/tiny-vans-draw.md deleted file mode 100644 index ca0e2f55ac5..00000000000 --- a/.changeset/tiny-vans-draw.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Mocks with an infinite delay no longer require result or error diff --git a/.size-limits.json b/.size-limits.json index 762fec516d0..8d2f9f6c9d7 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39042, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32550 + "dist/apollo-client.min.cjs": 39075, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32584 } diff --git a/CHANGELOG.md b/CHANGELOG.md index 96ce658db56..a9043830d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # @apollo/client +## 3.9.5 + +### Patch Changes + +- [#11595](https://github.com/apollographql/apollo-client/pull/11595) [`8c20955`](https://github.com/apollographql/apollo-client/commit/8c20955874562e5b2ab35557325e047b059bc4fc) Thanks [@phryneas](https://github.com/phryneas)! - Bumps the dependency `rehackt` to 0.0.5 + +- [#11592](https://github.com/apollographql/apollo-client/pull/11592) [`1133469`](https://github.com/apollographql/apollo-client/commit/1133469bd91ff76b9815e815a454a79d8e23a9bc) Thanks [@Stephen2](https://github.com/Stephen2)! - Strengthen `MockedResponse.newData` type + +- [#11579](https://github.com/apollographql/apollo-client/pull/11579) [`1ba2fd9`](https://github.com/apollographql/apollo-client/commit/1ba2fd919f79dfdc7b9d3f7d1a7aa5918e648349) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix issue where partial data is reported to `useQuery` when using `notifyOnNetworkStatusChange` after it errors while another overlapping query succeeds. + +- [#11579](https://github.com/apollographql/apollo-client/pull/11579) [`1ba2fd9`](https://github.com/apollographql/apollo-client/commit/1ba2fd919f79dfdc7b9d3f7d1a7aa5918e648349) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix an issue where a partial cache write for an errored query would result in automatically refetching that query. + +- [#11562](https://github.com/apollographql/apollo-client/pull/11562) [`65ab695`](https://github.com/apollographql/apollo-client/commit/65ab695470741e8dcaef1ebd7742c3c397526354) Thanks [@mspiess](https://github.com/mspiess)! - Mocks with an infinite delay no longer require result or error + ## 3.9.4 ### Patch Changes diff --git a/docs/source/api/core/ApolloClient.mdx b/docs/source/api/core/ApolloClient.mdx index a66e5713d6d..752f6633d9e 100644 --- a/docs/source/api/core/ApolloClient.mdx +++ b/docs/source/api/core/ApolloClient.mdx @@ -16,7 +16,7 @@ The `ApolloClient` class encapsulates Apollo's core client-side API. It backs al -Takes an `ApolloClientOptions` parameter that supports the [fields listed below](#ApolloClientOptions). +Takes an `ApolloClientOptions` parameter that supports the [fields listed below](#apolloclientoptions). Returns an initialized `ApolloClient` object. @@ -24,7 +24,7 @@ Returns an initialized `ApolloClient` object. -For more information on the `defaultOptions` object, see the [Default Options](#DefaultOptions) section below. +For more information on the `defaultOptions` object, see the [Default Options](#defaultoptions) section below. ## Functions diff --git a/package-lock.json b/package-lock.json index 83570ec627c..7c1fb2025ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.4", + "version": "3.9.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.4", + "version": "3.9.5", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -18,7 +18,7 @@ "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", - "rehackt": "0.0.4", + "rehackt": "0.0.5", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", @@ -26,12 +26,12 @@ "zen-observable-ts": "^1.2.5" }, "devDependencies": { - "@arethetypeswrong/cli": "0.13.8", + "@arethetypeswrong/cli": "0.13.10", "@babel/parser": "7.23.9", "@changesets/changelog-github": "0.5.0", "@changesets/cli": "2.27.1", "@graphql-tools/schema": "10.0.2", - "@microsoft/api-extractor": "7.40.1", + "@microsoft/api-extractor": "7.40.2", "@rollup/plugin-node-resolve": "11.2.1", "@size-limit/esbuild-why": "11.0.2", "@size-limit/preset-small-lib": "11.0.2", @@ -46,9 +46,9 @@ "@types/hoist-non-react-statics": "3.3.5", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/node": "20.11.17", + "@types/node": "20.11.19", "@types/node-fetch": "2.6.11", - "@types/react": "18.2.55", + "@types/react": "18.2.56", "@types/react-dom": "18.2.19", "@types/relay-runtime": "14.1.23", "@types/use-sync-external-store": "0.0.6", @@ -91,7 +91,7 @@ "rxjs": "7.8.1", "size-limit": "11.0.2", "subscriptions-transport-ws": "0.11.0", - "terser": "5.27.0", + "terser": "5.27.1", "ts-api-utils": "1.2.1", "ts-jest": "29.1.2", "ts-jest-resolver": "2.0.1", @@ -100,7 +100,7 @@ "typedoc": "0.25.0", "typescript": "5.3.3", "wait-for-observables": "1.0.3", - "web-streams-polyfill": "3.3.2", + "web-streams-polyfill": "3.3.3", "whatwg-fetch": "3.6.20" }, "engines": { @@ -163,12 +163,12 @@ "dev": true }, "node_modules/@arethetypeswrong/cli": { - "version": "0.13.8", - "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.13.8.tgz", - "integrity": "sha512-tQCXVBLuSfFlXdQLl17ZlBuHB1zo03H7uTLDFumoOl/dibXw1osYJ+Fd9Oju34buFDzI1DuFS2vba/Bsk95E5Q==", + "version": "0.13.10", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.13.10.tgz", + "integrity": "sha512-UV5Vk2ZaERQb0jS82rVWviKQieVpodiWKTOE/l8QN5fvG83AdhpI4QG8I5F1qSU/xq3K5RK44Bbgn6/JqwlT/A==", "dev": true, "dependencies": { - "@arethetypeswrong/core": "0.13.6", + "@arethetypeswrong/core": "0.13.9", "chalk": "^4.1.2", "cli-table3": "^0.6.3", "commander": "^10.0.1", @@ -220,13 +220,13 @@ } }, "node_modules/@arethetypeswrong/core": { - "version": "0.13.6", - "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.13.6.tgz", - "integrity": "sha512-e3CHQUK1aIIk8VOUavXPu3aVie3ZpxSGQHQoeBabzy81T4xWfQDrc68CqFmfGIEr8Apug47Yq+pYkCG2lsS10w==", + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.13.9.tgz", + "integrity": "sha512-JBvXC6fgHFWnTh0wrVsHbl+GPLdwD2IracDpHLvCLlnL4hx6fLCOdZL6Q4qdPAodcsehDL7M2fIrCMluk7kDeQ==", "dev": true, "dependencies": { "@andrewbranch/untar.js": "^1.0.3", - "fflate": "^0.7.4", + "fflate": "^0.8.2", "semver": "^7.5.4", "ts-expose-internals-conditionally": "1.0.0-empty.0", "typescript": "5.3.3", @@ -2518,17 +2518,17 @@ } }, "node_modules/@microsoft/api-extractor": { - "version": "7.40.1", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.40.1.tgz", - "integrity": "sha512-xHn2Zkh6s5JIjP94SG6VtIlIeRJcASgfZpDKV+bgoddMt1X4ujSZFOz7uEGNYNO7mEtdVOvpNKBpC4CDytD8KQ==", + "version": "7.40.2", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.40.2.tgz", + "integrity": "sha512-BCK+a9r0Nl/fd9fGhotaXJBt9IHBtuvEf/a8YS2UXwcqI4lnGcrvT3pAt3rrziS/dc5+0W/7TDZorULSj6N1Aw==", "dev": true, "dependencies": { - "@microsoft/api-extractor-model": "7.28.9", + "@microsoft/api-extractor-model": "7.28.10", "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.66.0", - "@rushstack/rig-package": "0.5.1", - "@rushstack/ts-command-line": "4.17.1", + "@rushstack/node-core-library": "3.66.1", + "@rushstack/rig-package": "0.5.2", + "@rushstack/ts-command-line": "4.17.2", "colors": "~1.2.1", "lodash": "~4.17.15", "resolve": "~1.22.1", @@ -2541,14 +2541,14 @@ } }, "node_modules/@microsoft/api-extractor-model": { - "version": "7.28.9", - "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.9.tgz", - "integrity": "sha512-lM77dV+VO46MGp5lu4stUBnO3jyr+CrDzU+DtapcOQEZUqJxPYUoK5zjeD+gRZ9ckgGMZC94ch6FBkpmsjwQgw==", + "version": "7.28.10", + "resolved": "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.28.10.tgz", + "integrity": "sha512-5ThitnV04Jbo0337Q0/VOjeGdx0OiduGgx4aGzfD6gsTSppYCPQjm2eIygRfc7w+XIP33osAZsWHAOo419PGQg==", "dev": true, "dependencies": { "@microsoft/tsdoc": "0.14.2", "@microsoft/tsdoc-config": "~0.16.1", - "@rushstack/node-core-library": "3.66.0" + "@rushstack/node-core-library": "3.66.1" } }, "node_modules/@microsoft/api-extractor/node_modules/semver": { @@ -2686,9 +2686,9 @@ "dev": true }, "node_modules/@rushstack/node-core-library": { - "version": "3.66.0", - "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.66.0.tgz", - "integrity": "sha512-nXyddNe3T9Ph14TrIfjtLZ+GDzC7HL/wF+ZKC18qmRVtz2xXLd1ZzreVgiAgGDwn8ZUWZ/7q//gQJk96iWjSrg==", + "version": "3.66.1", + "resolved": "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.66.1.tgz", + "integrity": "sha512-ker69cVKAoar7MMtDFZC4CzcDxjwqIhFzqEnYI5NRN/8M3om6saWCVx/A7vL2t/jFCJsnzQplRDqA7c78pytng==", "dev": true, "dependencies": { "colors": "~1.2.1", @@ -2724,9 +2724,9 @@ } }, "node_modules/@rushstack/rig-package": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.1.tgz", - "integrity": "sha512-pXRYSe29TjRw7rqxD4WS3HN/sRSbfr+tJs4a9uuaSIBAITbUggygdhuG0VrO0EO+QqH91GhYMN4S6KRtOEmGVA==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.5.2.tgz", + "integrity": "sha512-mUDecIJeH3yYGZs2a48k+pbhM6JYwWlgjs2Ca5f2n1G2/kgdgP9D/07oglEGf6mRyXEnazhEENeYTSNDRCwdqA==", "dev": true, "dependencies": { "resolve": "~1.22.1", @@ -2734,9 +2734,9 @@ } }, "node_modules/@rushstack/ts-command-line": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.17.1.tgz", - "integrity": "sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==", + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.17.2.tgz", + "integrity": "sha512-QS2S2nJo9zXq/+9Dk10LmvIFugMizI9IeQUH4jnhIcoaeqYlsv2fK830U+/gMKpI5vomXz19XMXfkUfZzO4R3A==", "dev": true, "dependencies": { "@types/argparse": "1.0.38", @@ -3387,9 +3387,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -3418,9 +3418,9 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.2.55", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.55.tgz", - "integrity": "sha512-Y2Tz5P4yz23brwm2d7jNon39qoAtMMmalOQv6+fEFt1mT+FcM3D841wDpoUvFXhaYenuROCy3FZYqdTjM7qVyA==", + "version": "18.2.56", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.56.tgz", + "integrity": "sha512-NpwHDMkS/EFZF2dONFQHgkPRwhvgq/OAvIaGQzxGSBmaeR++kTg6njr15Vatz0/2VcCEwJQFi6Jf4Q0qBu0rLA==", "dev": true, "dependencies": { "@types/prop-types": "*", @@ -6425,9 +6425,9 @@ } }, "node_modules/fflate": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", - "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", "dev": true }, "node_modules/file-entry-cache": { @@ -10687,9 +10687,9 @@ } }, "node_modules/rehackt": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.0.4.tgz", - "integrity": "sha512-xFroSGCbMEK/cTJVhq+c8l/AzIeMeojVyLqtZmr2jmIAFvePjapkCSGg9MnrcNk68HPaMxGf+Ndqozotu78ITw==", + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.0.5.tgz", + "integrity": "sha512-BI1rV+miEkaHj8zd2n+gaMgzu/fKz7BGlb4zZ6HAiY9adDmJMkaDcmuXlJFv0eyKUob+oszs3/2gdnXUrzx2Tg==", "peerDependencies": { "@types/react": "*", "react": "*" @@ -11773,9 +11773,9 @@ } }, "node_modules/terser": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", - "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "version": "5.27.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.1.tgz", + "integrity": "sha512-29wAr6UU/oQpnTw5HoadwjUZnFQXGdOfj0LjZ4sVxzqwHh/QVkvr7m8y9WoR4iN3FRitVduTc6KdjcW38Npsug==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -12508,9 +12508,9 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", - "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", "dev": true, "engines": { "node": ">= 8" diff --git a/package.json b/package.json index 46e9671abdb..7b1841779db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.4", + "version": "3.9.5", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ @@ -99,7 +99,7 @@ "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", - "rehackt": "0.0.4", + "rehackt": "0.0.5", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", @@ -107,12 +107,12 @@ "zen-observable-ts": "^1.2.5" }, "devDependencies": { - "@arethetypeswrong/cli": "0.13.8", + "@arethetypeswrong/cli": "0.13.10", "@babel/parser": "7.23.9", "@changesets/changelog-github": "0.5.0", "@changesets/cli": "2.27.1", "@graphql-tools/schema": "10.0.2", - "@microsoft/api-extractor": "7.40.1", + "@microsoft/api-extractor": "7.40.2", "@rollup/plugin-node-resolve": "11.2.1", "@size-limit/esbuild-why": "11.0.2", "@size-limit/preset-small-lib": "11.0.2", @@ -127,9 +127,9 @@ "@types/hoist-non-react-statics": "3.3.5", "@types/jest": "29.5.12", "@types/lodash": "4.14.202", - "@types/node": "20.11.17", + "@types/node": "20.11.19", "@types/node-fetch": "2.6.11", - "@types/react": "18.2.55", + "@types/react": "18.2.56", "@types/react-dom": "18.2.19", "@types/relay-runtime": "14.1.23", "@types/use-sync-external-store": "0.0.6", @@ -172,7 +172,7 @@ "rxjs": "7.8.1", "size-limit": "11.0.2", "subscriptions-transport-ws": "0.11.0", - "terser": "5.27.0", + "terser": "5.27.1", "ts-api-utils": "1.2.1", "ts-jest": "29.1.2", "ts-jest-resolver": "2.0.1", @@ -181,7 +181,7 @@ "typedoc": "0.25.0", "typescript": "5.3.3", "wait-for-observables": "1.0.3", - "web-streams-polyfill": "3.3.2", + "web-streams-polyfill": "3.3.3", "whatwg-fetch": "3.6.20" }, "publishConfig": { diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index c9c62973d90..8863d7415ee 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -210,7 +210,28 @@ export class QueryInfo { setDiff(diff: Cache.DiffResult | null) { const oldDiff = this.lastDiff && this.lastDiff.diff; + + // If we do not tolerate partial results, skip this update to prevent it + // from being reported. This prevents a situtuation where a query that + // errors and another succeeds with overlapping data does not report the + // partial data result to the errored query. + // + // See https://github.com/apollographql/apollo-client/issues/11400 for more + // information on this issue. + if ( + diff && + !diff.complete && + !this.observableQuery?.options.returnPartialData && + // In the case of a cache eviction, the diff will become partial so we + // schedule a notification to send a network request (this.oqListener) to + // go and fetch the missing data. + !(oldDiff && oldDiff.complete) + ) { + return; + } + this.updateLastDiff(diff); + if (!this.dirty && !equal(oldDiff && oldDiff.result, diff && diff.result)) { this.dirty = true; if (!this.notifyTimeout) { diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index d25765e9c9b..98c1f735026 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -24,6 +24,7 @@ import { itAsync, MockLink, mockSingleLink, + MockSubscriptionLink, subscribeAndCount, wait, } from "../../testing"; @@ -2389,6 +2390,124 @@ describe("ObservableQuery", () => { } ); + it("handles multiple calls to getCurrentResult without losing data", async () => { + const query = gql` + { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); + + const obs = client.watchQuery({ query }); + const stream = new ObservableStream(obs); + + link.simulateResult({ + result: { + data: { + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }, + hasNext: true, + }, + }); + + { + const result = await stream.takeNext(); + expect(result.data).toEqual({ + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }); + } + + expect(obs.getCurrentResult().data).toEqual({ + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }); + + expect(obs.getCurrentResult().data).toEqual({ + greeting: { + message: "Hello world", + __typename: "Greeting", + }, + }); + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + { + const result = await stream.takeNext(); + expect(result.data).toEqual({ + greeting: { + message: "Hello world", + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", + }, + }); + } + + expect(obs.getCurrentResult().data).toEqual({ + greeting: { + message: "Hello world", + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", + }, + }); + + expect(obs.getCurrentResult().data).toEqual({ + greeting: { + message: "Hello world", + recipient: { + name: "Alice", + __typename: "Person", + }, + __typename: "Greeting", + }, + }); + }); + { type Result = Partial>; diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 072ab6d922c..072c3c9e5cb 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -26,8 +26,13 @@ import { import { QueryResult } from "../../types/types"; import { useQuery } from "../useQuery"; import { useMutation } from "../useMutation"; -import { profileHook, spyOnConsole } from "../../../testing/internal"; +import { + createProfiler, + profileHook, + spyOnConsole, +} from "../../../testing/internal"; import { useApolloClient } from "../useApolloClient"; +import { useLazyQuery } from "../useLazyQuery"; describe("useQuery Hook", () => { describe("General use", () => { @@ -4018,6 +4023,264 @@ describe("useQuery Hook", () => { }); }); + // https://github.com/apollographql/apollo-client/issues/11400 + it("does not return partial data unexpectedly when one query errors, then another succeeds with overlapping data", async () => { + interface Query1 { + person: { + __typename: "Person"; + id: number; + firstName: string; + alwaysFails: boolean; + } | null; + } + + interface Query2 { + person: { __typename: "Person"; id: number; lastName: string } | null; + } + + interface Variables { + id: number; + } + + const user = userEvent.setup(); + + const query1: TypedDocumentNode = gql` + query PersonQuery1($id: ID!) { + person(id: $id) { + id + firstName + alwaysFails + } + } + `; + + const query2: TypedDocumentNode = gql` + query PersonQuery2($id: ID!) { + person(id: $id) { + id + lastName + } + } + `; + + const Profiler = createProfiler({ + initialSnapshot: { + useQueryResult: null as QueryResult | null, + useLazyQueryResult: null as QueryResult | null, + }, + }); + + const client = new ApolloClient({ + link: new MockLink([ + { + request: { query: query1, variables: { id: 1 } }, + result: { + data: { person: null }, + errors: [new GraphQLError("Intentional error")], + }, + maxUsageCount: Number.POSITIVE_INFINITY, + delay: 20, + }, + { + request: { query: query2, variables: { id: 1 } }, + result: { + data: { person: { __typename: "Person", id: 1, lastName: "Doe" } }, + }, + delay: 20, + }, + ]), + cache: new InMemoryCache(), + }); + + function App() { + const useQueryResult = useQuery(query1, { + variables: { id: 1 }, + // This is necessary to reproduce the behavior + notifyOnNetworkStatusChange: true, + }); + + const [execute, useLazyQueryResult] = useLazyQuery(query2, { + variables: { id: 1 }, + }); + + Profiler.replaceSnapshot({ useQueryResult, useLazyQueryResult }); + + return ( + <> + + + + ); + } + + render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.useQueryResult).toMatchObject({ + data: undefined, + loading: true, + networkStatus: NetworkStatus.loading, + }); + + expect(snapshot.useLazyQueryResult).toMatchObject({ + called: false, + data: undefined, + loading: false, + networkStatus: NetworkStatus.ready, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.useQueryResult).toMatchObject({ + data: undefined, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Intentional error")], + }), + loading: false, + networkStatus: NetworkStatus.error, + }); + + expect(snapshot.useLazyQueryResult).toMatchObject({ + called: false, + data: undefined, + loading: false, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Run 2nd query"))); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.useQueryResult).toMatchObject({ + data: undefined, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Intentional error")], + }), + loading: false, + networkStatus: NetworkStatus.error, + }); + + expect(snapshot.useLazyQueryResult).toMatchObject({ + called: true, + data: undefined, + loading: true, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.useQueryResult).toMatchObject({ + data: undefined, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Intentional error")], + }), + loading: false, + networkStatus: NetworkStatus.error, + }); + + // ensure we aren't setting a value on the observable query that contains + // the partial result + expect( + snapshot.useQueryResult?.observable.getCurrentResult(false) + ).toEqual({ + data: undefined, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Intentional error")], + }), + errors: [new GraphQLError("Intentional error")], + loading: false, + networkStatus: NetworkStatus.error, + partial: true, + }); + + expect(snapshot.useLazyQueryResult).toMatchObject({ + called: true, + data: { person: { __typename: "Person", id: 1, lastName: "Doe" } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Reload 1st query"))); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.useQueryResult).toMatchObject({ + data: undefined, + loading: true, + networkStatus: NetworkStatus.loading, + }); + + expect(snapshot.useLazyQueryResult).toMatchObject({ + called: true, + data: { person: { __typename: "Person", id: 1, lastName: "Doe" } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.useQueryResult).toMatchObject({ + data: undefined, + loading: false, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Intentional error")], + }), + networkStatus: NetworkStatus.error, + }); + + // ensure we aren't setting a value on the observable query that contains + // the partial result + expect( + snapshot.useQueryResult?.observable.getCurrentResult(false) + ).toEqual({ + data: undefined, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Intentional error")], + }), + errors: [new GraphQLError("Intentional error")], + loading: false, + networkStatus: NetworkStatus.error, + partial: true, + }); + + expect(snapshot.useLazyQueryResult).toMatchObject({ + called: true, + data: { person: { __typename: "Person", id: 1, lastName: "Doe" } }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); + }); + describe("Refetching", () => { it("refetching with different variables", async () => { const query = gql` diff --git a/src/testing/core/mocking/__tests__/mockLink.ts b/src/testing/core/mocking/__tests__/mockLink.ts index ffac583ceee..dc68b654505 100644 --- a/src/testing/core/mocking/__tests__/mockLink.ts +++ b/src/testing/core/mocking/__tests__/mockLink.ts @@ -1,7 +1,71 @@ import gql from "graphql-tag"; -import { MockLink } from "../mockLink"; +import { MockLink, MockedResponse } from "../mockLink"; import { execute } from "../../../../link/core/execute"; +describe("MockedResponse.newData", () => { + const setup = () => { + const weaklyTypedMockResponse: MockedResponse = { + request: { + query: gql` + query A { + a + } + `, + }, + }; + + const stronglyTypedMockResponse: MockedResponse< + { a: string }, + { input: string } + > = { + request: { + query: gql` + query A { + a + } + `, + }, + }; + + return { + weaklyTypedMockResponse, + stronglyTypedMockResponse, + }; + }; + + test("returned 'data' can be any object with untyped response", () => { + const { weaklyTypedMockResponse } = setup(); + + weaklyTypedMockResponse.newData = ({ fake: { faker } }) => ({ + data: { + pretend: faker, + }, + }); + }); + + test("can't return output that doesn't match TData", () => { + const { stronglyTypedMockResponse } = setup(); + + // @ts-expect-error return type does not match `TData` + stronglyTypedMockResponse.newData = () => ({ + data: { + a: 123, + }, + }); + }); + + test("can't use input variables that don't exist in TVariables", () => { + const { stronglyTypedMockResponse } = setup(); + + // @ts-expect-error unknown variables + stronglyTypedMockResponse.newData = ({ fake: { faker } }) => ({ + data: { + a: faker, + }, + }); + }); +}); + /* We've chosen this value as the MAXIMUM_DELAY since values that don't fit into a 32-bit signed int cause setTimeout to fire immediately */ diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index fd580eee6ce..46b43cca6ad 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -35,7 +35,7 @@ export interface MockedResponse< error?: Error; delay?: number; variableMatcher?: VariableMatcher; - newData?: ResultFunction; + newData?: ResultFunction, TVariables>; } export interface MockLinkOptions {