From d5d8cb10c468a1f256286b354a428afd69765c15 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 16 Nov 2023 18:09:58 +0100 Subject: [PATCH 01/14] `print`: use `WeakCache` instead of `WeakMap` --- .changeset/polite-avocados-warn.md | 5 +++++ .size-limit.cjs | 5 +++-- package-lock.json | 12 ++++++++++++ package.json | 1 + src/utilities/graphql/print.ts | 13 +++++-------- 5 files changed, 26 insertions(+), 10 deletions(-) create mode 100644 .changeset/polite-avocados-warn.md diff --git a/.changeset/polite-avocados-warn.md b/.changeset/polite-avocados-warn.md new file mode 100644 index 00000000000..dd04015cf3d --- /dev/null +++ b/.changeset/polite-avocados-warn.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`print`: use `WeakCache` instead of `WeakMap` diff --git a/.size-limit.cjs b/.size-limit.cjs index 63819405fd1..e2233d201f3 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,7 +1,7 @@ const checks = [ { path: "dist/apollo-client.min.cjs", - limit: "38164", + limit: "38178", }, { path: "dist/main.cjs", @@ -10,7 +10,7 @@ const checks = [ { path: "dist/index.js", import: "{ ApolloClient, InMemoryCache, HttpLink }", - limit: "32188", + limit: "32206", }, ...[ "ApolloProvider", @@ -35,6 +35,7 @@ const checks = [ "react", "react-dom", "@graphql-typed-document-node/core", + "@wry/caches", "@wry/context", "@wry/equality", "@wry/trie", diff --git a/package-lock.json b/package-lock.json index 9cf8221c770..2217dbde199 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", "@wry/context": "^0.7.3", "@wry/equality": "^0.5.6", "@wry/trie": "^0.4.3", @@ -3294,6 +3295,17 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@wry/caches": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.0.tgz", + "integrity": "sha512-FHRUDe2tqrXAj6A/1D39No68lFWbbnh+NCpG9J/6idhL/2Mb/AaxBTYg/sbUVImEo8a4mWeOewUlB1W7uLjByA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/@wry/context": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.7.3.tgz", diff --git a/package.json b/package.json index 3a7d6f0196f..da96797c4ea 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ }, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", "@wry/context": "^0.7.3", "@wry/equality": "^0.5.6", "@wry/trie": "^0.4.3", diff --git a/src/utilities/graphql/print.ts b/src/utilities/graphql/print.ts index d90a15611d0..572584e4ca3 100644 --- a/src/utilities/graphql/print.ts +++ b/src/utilities/graphql/print.ts @@ -1,23 +1,20 @@ import type { ASTNode } from "graphql"; import { print as origPrint } from "graphql"; -import { canUseWeakMap } from "../common/canUse.js"; +import { WeakCache } from "@wry/caches" -let printCache: undefined | WeakMap; -// further TODO: replace with `optimism` with a `WeakCache` once those are available +let printCache!: WeakCache; export const print = Object.assign( (ast: ASTNode) => { - let result; - result = printCache?.get(ast); + let result = printCache.get(ast); if (!result) { - result = origPrint(ast); - printCache?.set(ast, result); + printCache.set(ast, result = origPrint(ast)); } return result; }, { reset() { - printCache = canUseWeakMap ? new WeakMap() : undefined; + printCache = new WeakCache(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); }, } ); From 29bf59da53b2846f9f713e93000c9d1d16d6b0a1 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Thu, 16 Nov 2023 18:11:41 +0100 Subject: [PATCH 02/14] format --- src/utilities/graphql/print.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/utilities/graphql/print.ts b/src/utilities/graphql/print.ts index 572584e4ca3..d536ff8bb77 100644 --- a/src/utilities/graphql/print.ts +++ b/src/utilities/graphql/print.ts @@ -1,6 +1,6 @@ import type { ASTNode } from "graphql"; import { print as origPrint } from "graphql"; -import { WeakCache } from "@wry/caches" +import { WeakCache } from "@wry/caches"; let printCache!: WeakCache; export const print = Object.assign( @@ -8,13 +8,16 @@ export const print = Object.assign( let result = printCache.get(ast); if (!result) { - printCache.set(ast, result = origPrint(ast)); + printCache.set(ast, (result = origPrint(ast))); } return result; }, { reset() { - printCache = new WeakCache(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); + printCache = new WeakCache< + ASTNode, + string + >(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); }, } ); From 9942e61166e182768fce7f0886f59677c14e3a4d Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 17 Nov 2023 12:51:58 +0100 Subject: [PATCH 03/14] pull in memory testing tools from PR 11358 --- package.json | 3 +- src/testing/matchers/index.d.ts | 9 ++++ src/testing/matchers/index.ts | 2 + src/testing/matchers/toBeGarbageCollected.ts | 51 ++++++++++++++++++++ src/tsconfig.json | 2 +- 5 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 src/testing/matchers/toBeGarbageCollected.ts diff --git a/package.json b/package.json index da96797c4ea..5f7353d178c 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "ci:precheck": "node config/precheck.js", "format": "prettier --write .", "lint": "eslint 'src/**/*.{[jt]s,[jt]sx}'", - "test": "jest --config ./config/jest.config.js", + "test": "node --expose-gc ./node_modules/jest/bin/jest.js --config ./config/jest.config.js", "test:debug": "node --inspect-brk node_modules/.bin/jest --config ./config/jest.config.js --runInBand --testTimeout 99999 --logHeapUsage", "test:ci": "TEST_ENV=ci npm run test:coverage -- --logHeapUsage && npm run test:memory", "test:watch": "jest --config ./config/jest.config.js --watch", @@ -89,7 +89,6 @@ }, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", - "@wry/caches": "^1.0.0", "@wry/context": "^0.7.3", "@wry/equality": "^0.5.6", "@wry/trie": "^0.4.3", diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 715f7d3dbdf..a8ea24c1023 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -9,6 +9,11 @@ import { ProfiledHook, } from "../internal/index.js"; +declare class WeakRef { + constructor(target: T); + deref(): T | undefined; +} + interface ApolloCustomMatchers { /** * Used to determine if two GraphQL query documents are equal to each other by @@ -38,6 +43,10 @@ interface ApolloCustomMatchers { | ProfiledHook ? (count: number, options?: NextRenderOptions) => Promise : { error: "matcher needs to be called on a ProfiledComponent instance" }; + + toBeGarbageCollected: T extends WeakRef + ? () => Promise + : { error: "matcher needs to be called on a WeakRef instance" }; } declare global { diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index d2ebd8ce7c2..709bfbad53b 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -2,10 +2,12 @@ import { expect } from "@jest/globals"; import { toMatchDocument } from "./toMatchDocument.js"; import { toHaveSuspenseCacheEntryUsing } from "./toHaveSuspenseCacheEntryUsing.js"; import { toRerender, toRenderExactlyTimes } from "./ProfiledComponent.js"; +import { toBeGarbageCollected } from "./toBeGarbageCollected.js"; expect.extend({ toHaveSuspenseCacheEntryUsing, toMatchDocument, toRerender, toRenderExactlyTimes, + toBeGarbageCollected, }); diff --git a/src/testing/matchers/toBeGarbageCollected.ts b/src/testing/matchers/toBeGarbageCollected.ts new file mode 100644 index 00000000000..baa9c59128b --- /dev/null +++ b/src/testing/matchers/toBeGarbageCollected.ts @@ -0,0 +1,51 @@ +import type { MatcherFunction } from "expect"; + +declare class WeakRef { + constructor(target: T); + deref(): T | undefined; +} + +export const toBeGarbageCollected: MatcherFunction<[weakRef: WeakRef]> = + async function (actual) { + const hint = this.utils.matcherHint("toBeGarbageCollected"); + + if (!(actual instanceof WeakRef)) { + throw new Error( + hint + + "\n\n" + + `Expected value to be a WeakRef, but it was a ${typeof actual}.` + ); + } + + let pass = false; + let interval: NodeJS.Timeout | undefined; + await Promise.race([ + new Promise((resolve) => setTimeout(resolve, 1000)), + new Promise((resolve) => { + setInterval(() => { + global.gc!(); + pass = actual.deref() === undefined; + if (pass) { + resolve(); + } + }, 1); + }), + ]).finally(() => clearInterval(interval)); + + return { + pass, + message: () => { + if (pass) { + return ( + hint + + "\n\n" + + "Expected value to not be cache-collected, but it was." + ); + } + + return ( + hint + "\n\n Expected value to be cache-collected, but it was not." + ); + }, + }; + }; diff --git a/src/tsconfig.json b/src/tsconfig.json index 321f038a735..40ade0f5761 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -5,7 +5,7 @@ { "compilerOptions": { "noEmit": true, - "lib": ["es2015", "esnext.asynciterable", "dom"], + "lib": ["es2015", "esnext.asynciterable", "dom", "ES2021.WeakRef"], "types": ["jest", "node", "./testing/matchers/index.d.ts"] }, "extends": "../tsconfig.json", From b933dfb00e352483c4308df6ce147c5e4da3ef5a Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 17 Nov 2023 14:42:47 +0100 Subject: [PATCH 04/14] Persisted Query Link: improve memory management --- .../api-report-link_persisted-queries.md | 4 +- .changeset/thick-tips-cry.md | 9 + .../__tests__/persisted-queries.test.ts | 61 ++++ src/link/persisted-queries/index.ts | 285 +++++++++--------- 4 files changed, 223 insertions(+), 136 deletions(-) create mode 100644 .changeset/thick-tips-cry.md diff --git a/.api-reports/api-report-link_persisted-queries.md b/.api-reports/api-report-link_persisted-queries.md index dda0d3dd1db..84c87032e4c 100644 --- a/.api-reports/api-report-link_persisted-queries.md +++ b/.api-reports/api-report-link_persisted-queries.md @@ -57,7 +57,9 @@ interface BaseOptions { // Warning: (ae-forgotten-export) The symbol "ApolloLink" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const createPersistedQueryLink: (options: PersistedQueryLink.Options) => ApolloLink; +export const createPersistedQueryLink: (options: PersistedQueryLink.Options) => ApolloLink & { + resetCache: () => void; +}; // @public (undocumented) interface DefaultContext extends Record { diff --git a/.changeset/thick-tips-cry.md b/.changeset/thick-tips-cry.md new file mode 100644 index 00000000000..407513ec1c7 --- /dev/null +++ b/.changeset/thick-tips-cry.md @@ -0,0 +1,9 @@ +--- +"@apollo/client": patch +--- + +Persisted Query Link: improve memory management +* use LRU `WeakCache` instead of `WeakMap` to keep a limited number of hash results +* hash cache is initiated lazily, only when needed +* expose `persistedLink.resetHashCache()` method +* reset hash cache if the upstream server reports it doesn't accept persisted queries diff --git a/src/link/persisted-queries/__tests__/persisted-queries.test.ts b/src/link/persisted-queries/__tests__/persisted-queries.test.ts index ea8b56e660a..bb3b8cb8c44 100644 --- a/src/link/persisted-queries/__tests__/persisted-queries.test.ts +++ b/src/link/persisted-queries/__tests__/persisted-queries.test.ts @@ -151,6 +151,32 @@ describe("happy path", () => { }, reject); }); + it("clears the cache when calling `resetHashCache`", async () => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); + + const hashRefs: WeakRef[] = []; + function hash(query: string) { + const newHash = new String(query); + hashRefs.push(new WeakRef(newHash)); + return newHash as string; + } + const persistedLink = createPersistedQuery({ sha256: hash }); + await new Promise((complete) => + execute(persistedLink.concat(createHttpLink()), { + query, + variables, + }).subscribe({ complete }) + ); + + await expect(hashRefs[0]).not.toBeGarbageCollected(); + persistedLink.resetHashCache(); + await expect(hashRefs[0]).toBeGarbageCollected(); + }); + itAsync("supports loading the hash from other method", (resolve, reject) => { fetchMock.post( "/graphql", @@ -517,6 +543,41 @@ describe("failure path", () => { }) ); + it.each([ + ["error message", giveUpResponse], + ["error code", giveUpResponseWithCode], + ] as const)( + "clears the cache when receiving NotSupported error (%s)", + async (_description, failingResponse) => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: failingResponse })), + { repeat: 1 } + ); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); + + const hashRefs: WeakRef[] = []; + function hash(query: string) { + const newHash = new String(query); + hashRefs.push(new WeakRef(newHash)); + return newHash as string; + } + const persistedLink = createPersistedQuery({ sha256: hash }); + await new Promise((complete) => + execute(persistedLink.concat(createHttpLink()), { + query, + variables, + }).subscribe({ complete }) + ); + + await expect(hashRefs[0]).toBeGarbageCollected(); + } + ); + itAsync("works with multiple errors", (resolve, reject) => { fetchMock.post( "/graphql", diff --git a/src/link/persisted-queries/index.ts b/src/link/persisted-queries/index.ts index b2a8c97fbce..50a2aa5ca7e 100644 --- a/src/link/persisted-queries/index.ts +++ b/src/link/persisted-queries/index.ts @@ -12,6 +12,7 @@ import type { import { Observable, compact, isNonEmptyArray } from "../../utilities/index.js"; import type { NetworkError } from "../../errors/index.js"; import type { ServerError } from "../utils/index.js"; +import { WeakCache } from "@wry/caches"; export const VERSION = 1; @@ -93,7 +94,10 @@ function operationDefinesMutation(operation: Operation) { export const createPersistedQueryLink = ( options: PersistedQueryLink.Options ) => { - const hashesByQuery = new WeakMap>(); + let hashesByQuery: WeakCache> | undefined; + function resetHashCache() { + hashesByQuery = undefined; + } // Ensure a SHA-256 hash function is provided, if a custom hash // generation function is not provided. We don't supply a SHA-256 hash // function by default, to avoid forcing one as a dependency. Developers @@ -135,150 +139,161 @@ export const createPersistedQueryLink = ( // what to do with the bogus query. return getHashPromise(query); } + if (!hashesByQuery) { + hashesByQuery = + new WeakCache(/** TODO: decide on a maximum size (will do all max sizes in a combined separate PR) */); + } let hash = hashesByQuery.get(query)!; if (!hash) hashesByQuery.set(query, (hash = getHashPromise(query))); return hash; } - return new ApolloLink((operation, forward) => { - invariant( - forward, - "PersistedQueryLink cannot be the last link in the chain." - ); - - const { query } = operation; - - return new Observable((observer: Observer) => { - let subscription: ObservableSubscription; - let retried = false; - let originalFetchOptions: any; - let setFetchOptions = false; - const maybeRetry = ( - { - response, - networkError, - }: { response?: ExecutionResult; networkError?: ServerError }, - cb: () => void - ) => { - if (!retried && ((response && response.errors) || networkError)) { - retried = true; - - const graphQLErrors: GraphQLError[] = []; - - const responseErrors = response && response.errors; - if (isNonEmptyArray(responseErrors)) { - graphQLErrors.push(...responseErrors); - } - - // Network errors can return GraphQL errors on for example a 403 - let networkErrors; - if (typeof networkError?.result !== "string") { - networkErrors = - networkError && - networkError.result && - (networkError.result.errors as GraphQLError[]); - } - if (isNonEmptyArray(networkErrors)) { - graphQLErrors.push(...networkErrors); - } - - const disablePayload: ErrorResponse = { + return Object.assign( + new ApolloLink((operation, forward) => { + invariant( + forward, + "PersistedQueryLink cannot be the last link in the chain." + ); + + const { query } = operation; + + return new Observable((observer: Observer) => { + let subscription: ObservableSubscription; + let retried = false; + let originalFetchOptions: any; + let setFetchOptions = false; + const maybeRetry = ( + { response, networkError, - operation, - graphQLErrors: isNonEmptyArray(graphQLErrors) - ? graphQLErrors - : void 0, - meta: processErrors(graphQLErrors), - }; - - // if the server doesn't support persisted queries, don't try anymore - supportsPersistedQueries = !disable(disablePayload); - - // if its not found, we can try it again, otherwise just report the error - if (retry(disablePayload)) { - // need to recall the link chain - if (subscription) subscription.unsubscribe(); - // actually send the query this time - operation.setContext({ - http: { - includeQuery: true, - includeExtensions: supportsPersistedQueries, - }, - fetchOptions: { - // Since we're including the full query, which may be - // large, we should send it in the body of a POST request. - // See issue #7456. - method: "POST", - }, - }); - if (setFetchOptions) { - operation.setContext({ fetchOptions: originalFetchOptions }); + }: { response?: ExecutionResult; networkError?: ServerError }, + cb: () => void + ) => { + if (!retried && ((response && response.errors) || networkError)) { + retried = true; + + const graphQLErrors: GraphQLError[] = []; + + const responseErrors = response && response.errors; + if (isNonEmptyArray(responseErrors)) { + graphQLErrors.push(...responseErrors); } - subscription = forward(operation).subscribe(handler); - return; - } - } - cb(); - }; - const handler = { - next: (response: ExecutionResult) => { - maybeRetry({ response }, () => observer.next!(response)); - }, - error: (networkError: ServerError) => { - maybeRetry({ networkError }, () => observer.error!(networkError)); - }, - complete: observer.complete!.bind(observer), - }; - - // don't send the query the first time - operation.setContext({ - http: { - includeQuery: !supportsPersistedQueries, - includeExtensions: supportsPersistedQueries, - }, - }); + // Network errors can return GraphQL errors on for example a 403 + let networkErrors; + if (typeof networkError?.result !== "string") { + networkErrors = + networkError && + networkError.result && + (networkError.result.errors as GraphQLError[]); + } + if (isNonEmptyArray(networkErrors)) { + graphQLErrors.push(...networkErrors); + } - // If requested, set method to GET if there are no mutations. Remember the - // original fetchOptions so we can restore them if we fall back to a - // non-hashed request. - if ( - useGETForHashedQueries && - supportsPersistedQueries && - !operationDefinesMutation(operation) - ) { - operation.setContext( - ({ fetchOptions = {} }: { fetchOptions: Record }) => { - originalFetchOptions = fetchOptions; - return { - fetchOptions: { - ...fetchOptions, - method: "GET", - }, + const disablePayload: ErrorResponse = { + response, + networkError, + operation, + graphQLErrors: isNonEmptyArray(graphQLErrors) + ? graphQLErrors + : void 0, + meta: processErrors(graphQLErrors), }; + + // if the server doesn't support persisted queries, don't try anymore + supportsPersistedQueries = !disable(disablePayload); + if (!supportsPersistedQueries) { + // clear hashes from cache, we don't need them anymore + resetHashCache(); + } + + // if its not found, we can try it again, otherwise just report the error + if (retry(disablePayload)) { + // need to recall the link chain + if (subscription) subscription.unsubscribe(); + // actually send the query this time + operation.setContext({ + http: { + includeQuery: true, + includeExtensions: supportsPersistedQueries, + }, + fetchOptions: { + // Since we're including the full query, which may be + // large, we should send it in the body of a POST request. + // See issue #7456. + method: "POST", + }, + }); + if (setFetchOptions) { + operation.setContext({ fetchOptions: originalFetchOptions }); + } + subscription = forward(operation).subscribe(handler); + + return; + } } - ); - setFetchOptions = true; - } - - if (supportsPersistedQueries) { - getQueryHash(query) - .then((sha256Hash) => { - operation.extensions.persistedQuery = { - version: VERSION, - sha256Hash, - }; - subscription = forward(operation).subscribe(handler); - }) - .catch(observer.error!.bind(observer)); - } else { - subscription = forward(operation).subscribe(handler); - } - - return () => { - if (subscription) subscription.unsubscribe(); - }; - }); - }); + cb(); + }; + const handler = { + next: (response: ExecutionResult) => { + maybeRetry({ response }, () => observer.next!(response)); + }, + error: (networkError: ServerError) => { + maybeRetry({ networkError }, () => observer.error!(networkError)); + }, + complete: observer.complete!.bind(observer), + }; + + // don't send the query the first time + operation.setContext({ + http: { + includeQuery: !supportsPersistedQueries, + includeExtensions: supportsPersistedQueries, + }, + }); + + // If requested, set method to GET if there are no mutations. Remember the + // original fetchOptions so we can restore them if we fall back to a + // non-hashed request. + if ( + useGETForHashedQueries && + supportsPersistedQueries && + !operationDefinesMutation(operation) + ) { + operation.setContext( + ({ fetchOptions = {} }: { fetchOptions: Record }) => { + originalFetchOptions = fetchOptions; + return { + fetchOptions: { + ...fetchOptions, + method: "GET", + }, + }; + } + ); + setFetchOptions = true; + } + + if (supportsPersistedQueries) { + getQueryHash(query) + .then((sha256Hash) => { + operation.extensions.persistedQuery = { + version: VERSION, + sha256Hash, + }; + subscription = forward(operation).subscribe(handler); + }) + .catch(observer.error!.bind(observer)); + } else { + subscription = forward(operation).subscribe(handler); + } + + return () => { + if (subscription) subscription.unsubscribe(); + }; + }); + }), + { resetHashCache } + ); }; From a117bd0cc780d5c61878b67a589ff971b856c0bb Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 17 Nov 2023 14:46:32 +0100 Subject: [PATCH 05/14] re-add accidentally removed dependency --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 5f7353d178c..24daea70d79 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ }, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", "@wry/context": "^0.7.3", "@wry/equality": "^0.5.6", "@wry/trie": "^0.4.3", From 0a1b7187c0c67fa8448af454058c32aa3e28fdb0 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 17 Nov 2023 15:00:29 +0100 Subject: [PATCH 06/14] update api --- .api-reports/api-report-link_persisted-queries.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.api-reports/api-report-link_persisted-queries.md b/.api-reports/api-report-link_persisted-queries.md index 84c87032e4c..353d48364e6 100644 --- a/.api-reports/api-report-link_persisted-queries.md +++ b/.api-reports/api-report-link_persisted-queries.md @@ -58,7 +58,7 @@ interface BaseOptions { // // @public (undocumented) export const createPersistedQueryLink: (options: PersistedQueryLink.Options) => ApolloLink & { - resetCache: () => void; + resetHashCache: () => void; }; // @public (undocumented) From bd40e8e4f16b6ae69249d41c539eb16bfa72794c Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 17 Nov 2023 15:48:22 +0100 Subject: [PATCH 07/14] update size limit --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index f71b98fe37c..8a945778178 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 37975, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32019 + "dist/apollo-client.min.cjs": 38178, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32206 } From f84da806411314c19a9fda8d76bf1e9bd46833c1 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Fri, 17 Nov 2023 15:56:17 +0100 Subject: [PATCH 08/14] size-limit --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index 52108cf4fb9..8a945778178 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38164, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32188 + "dist/apollo-client.min.cjs": 38178, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32206 } From 6ad8e56a7b04ae5f7823d20ddaf92d3835f742b4 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 Nov 2023 14:44:47 +0100 Subject: [PATCH 09/14] fix test failure --- .../persisted-queries/__tests__/persisted-queries.test.ts | 2 +- src/link/persisted-queries/__tests__/react.test.tsx | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/link/persisted-queries/__tests__/persisted-queries.test.ts b/src/link/persisted-queries/__tests__/persisted-queries.test.ts index bb3b8cb8c44..32d75fe5136 100644 --- a/src/link/persisted-queries/__tests__/persisted-queries.test.ts +++ b/src/link/persisted-queries/__tests__/persisted-queries.test.ts @@ -66,7 +66,7 @@ const giveUpResponse = JSON.stringify({ errors: giveUpErrors }); const giveUpResponseWithCode = JSON.stringify({ errors: giveUpErrorsWithCode }); const multiResponse = JSON.stringify({ errors: multipleErrors }); -export function sha256(data: string) { +function sha256(data: string) { const hash = crypto.createHash("sha256"); hash.update(data); return hash.digest("hex"); diff --git a/src/link/persisted-queries/__tests__/react.test.tsx b/src/link/persisted-queries/__tests__/react.test.tsx index 07c3fe7375e..b05e7d98f32 100644 --- a/src/link/persisted-queries/__tests__/react.test.tsx +++ b/src/link/persisted-queries/__tests__/react.test.tsx @@ -4,6 +4,7 @@ import * as ReactDOM from "react-dom/server"; import gql from "graphql-tag"; import { print } from "graphql"; import fetchMock from "fetch-mock"; +import crypto from "crypto"; import { ApolloProvider } from "../../../react/context"; import { InMemoryCache as Cache } from "../../../cache/inmemory/inMemoryCache"; @@ -12,7 +13,12 @@ import { createHttpLink } from "../../http/createHttpLink"; import { graphql } from "../../../react/hoc/graphql"; import { getDataFromTree } from "../../../react/ssr/getDataFromTree"; import { createPersistedQueryLink as createPersistedQuery, VERSION } from ".."; -import { sha256 } from "./persisted-queries.test"; + +function sha256(data: string) { + const hash = crypto.createHash("sha256"); + hash.update(data); + return hash.digest("hex"); +} // Necessary configuration in order to mock multiple requests // to a single (/graphql) endpoint From 7cbf8dc77a2243ec17d96d35e5d4f7bbe6c7b9c5 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 29 Nov 2023 14:45:16 +0100 Subject: [PATCH 10/14] better cleanup of interval/timeout --- src/testing/matchers/toBeGarbageCollected.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/testing/matchers/toBeGarbageCollected.ts b/src/testing/matchers/toBeGarbageCollected.ts index baa9c59128b..4dee2956121 100644 --- a/src/testing/matchers/toBeGarbageCollected.ts +++ b/src/testing/matchers/toBeGarbageCollected.ts @@ -19,10 +19,13 @@ export const toBeGarbageCollected: MatcherFunction<[weakRef: WeakRef]> = let pass = false; let interval: NodeJS.Timeout | undefined; + let timeout: NodeJS.Timeout | undefined; await Promise.race([ - new Promise((resolve) => setTimeout(resolve, 1000)), new Promise((resolve) => { - setInterval(() => { + timeout = setTimeout(resolve, 1000); + }), + new Promise((resolve) => { + interval = setInterval(() => { global.gc!(); pass = actual.deref() === undefined; if (pass) { @@ -30,7 +33,10 @@ export const toBeGarbageCollected: MatcherFunction<[weakRef: WeakRef]> = } }, 1); }), - ]).finally(() => clearInterval(interval)); + ]); + + clearInterval(interval); + clearTimeout(timeout); return { pass, From 0521529f22e262ce42f1497a93c968678e3f6ee8 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 4 Dec 2023 10:34:50 +0100 Subject: [PATCH 11/14] apply formatting --- src/link/persisted-queries/index.ts | 5 ++-- src/testing/matchers/index.d.ts | 45 ++++++++++++++--------------- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/link/persisted-queries/index.ts b/src/link/persisted-queries/index.ts index 50a2aa5ca7e..95c4129c802 100644 --- a/src/link/persisted-queries/index.ts +++ b/src/link/persisted-queries/index.ts @@ -195,9 +195,8 @@ export const createPersistedQueryLink = ( response, networkError, operation, - graphQLErrors: isNonEmptyArray(graphQLErrors) - ? graphQLErrors - : void 0, + graphQLErrors: + isNonEmptyArray(graphQLErrors) ? graphQLErrors : void 0, meta: processErrors(graphQLErrors), }; diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index d933163abee..6936be4804c 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -25,33 +25,30 @@ interface ApolloCustomMatchers { /** * Used to determine if the Suspense cache has a cache entry. */ - toHaveSuspenseCacheEntryUsing: T extends ApolloClient - ? ( - query: DocumentNode, - options?: { - variables?: OperationVariables; - queryKey?: string | number | any[]; - } - ) => R - : { error: "matcher needs to be called on an ApolloClient instance" }; + toHaveSuspenseCacheEntryUsing: T extends ApolloClient ? + ( + query: DocumentNode, + options?: { + variables?: OperationVariables; + queryKey?: string | number | any[]; + } + ) => R + : { error: "matcher needs to be called on an ApolloClient instance" }; - toRerender: T extends - | Profiler - | ProfiledComponent - | ProfiledHook - ? (options?: NextRenderOptions) => Promise - : { error: "matcher needs to be called on a ProfiledComponent instance" }; + toRerender: T extends ( + Profiler | ProfiledComponent | ProfiledHook + ) ? + (options?: NextRenderOptions) => Promise + : { error: "matcher needs to be called on a ProfiledComponent instance" }; - toRenderExactlyTimes: T extends - | Profiler - | ProfiledComponent - | ProfiledHook - ? (count: number, options?: NextRenderOptions) => Promise - : { error: "matcher needs to be called on a ProfiledComponent instance" }; + toRenderExactlyTimes: T extends ( + Profiler | ProfiledComponent | ProfiledHook + ) ? + (count: number, options?: NextRenderOptions) => Promise + : { error: "matcher needs to be called on a ProfiledComponent instance" }; - toBeGarbageCollected: T extends WeakRef - ? () => Promise - : { error: "matcher needs to be called on a WeakRef instance" }; + toBeGarbageCollected: T extends WeakRef ? () => Promise + : { error: "matcher needs to be called on a WeakRef instance" }; } declare global { From 188dd0ae17f406cf9d95a86550fdcee38881facd Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 4 Dec 2023 10:35:53 +0100 Subject: [PATCH 12/14] remove unneccessary type --- src/testing/matchers/index.d.ts | 50 ++++++++++++++++----------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 6936be4804c..65b10ba4fc8 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -10,11 +10,6 @@ import { ProfiledHook, } from "../internal/index.js"; -declare class WeakRef { - constructor(target: T); - deref(): T | undefined; -} - interface ApolloCustomMatchers { /** * Used to determine if two GraphQL query documents are equal to each other by @@ -25,30 +20,33 @@ interface ApolloCustomMatchers { /** * Used to determine if the Suspense cache has a cache entry. */ - toHaveSuspenseCacheEntryUsing: T extends ApolloClient ? - ( - query: DocumentNode, - options?: { - variables?: OperationVariables; - queryKey?: string | number | any[]; - } - ) => R - : { error: "matcher needs to be called on an ApolloClient instance" }; + toHaveSuspenseCacheEntryUsing: T extends ApolloClient + ? ( + query: DocumentNode, + options?: { + variables?: OperationVariables; + queryKey?: string | number | any[]; + } + ) => R + : { error: "matcher needs to be called on an ApolloClient instance" }; - toRerender: T extends ( - Profiler | ProfiledComponent | ProfiledHook - ) ? - (options?: NextRenderOptions) => Promise - : { error: "matcher needs to be called on a ProfiledComponent instance" }; + toRerender: T extends + | Profiler + | ProfiledComponent + | ProfiledHook + ? (options?: NextRenderOptions) => Promise + : { error: "matcher needs to be called on a ProfiledComponent instance" }; - toRenderExactlyTimes: T extends ( - Profiler | ProfiledComponent | ProfiledHook - ) ? - (count: number, options?: NextRenderOptions) => Promise - : { error: "matcher needs to be called on a ProfiledComponent instance" }; + toRenderExactlyTimes: T extends + | Profiler + | ProfiledComponent + | ProfiledHook + ? (count: number, options?: NextRenderOptions) => Promise + : { error: "matcher needs to be called on a ProfiledComponent instance" }; - toBeGarbageCollected: T extends WeakRef ? () => Promise - : { error: "matcher needs to be called on a WeakRef instance" }; + toBeGarbageCollected: T extends WeakRef + ? () => Promise + : { error: "matcher needs to be called on a WeakRef instance" }; } declare global { From 4283908f5de8971bba1b41f973a354222cb88513 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Mon, 4 Dec 2023 10:36:42 +0100 Subject: [PATCH 13/14] format again after updating prettier --- src/testing/matchers/index.d.ts | 45 +++++++++++++++------------------ 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 65b10ba4fc8..690589af128 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -20,33 +20,30 @@ interface ApolloCustomMatchers { /** * Used to determine if the Suspense cache has a cache entry. */ - toHaveSuspenseCacheEntryUsing: T extends ApolloClient - ? ( - query: DocumentNode, - options?: { - variables?: OperationVariables; - queryKey?: string | number | any[]; - } - ) => R - : { error: "matcher needs to be called on an ApolloClient instance" }; + toHaveSuspenseCacheEntryUsing: T extends ApolloClient ? + ( + query: DocumentNode, + options?: { + variables?: OperationVariables; + queryKey?: string | number | any[]; + } + ) => R + : { error: "matcher needs to be called on an ApolloClient instance" }; - toRerender: T extends - | Profiler - | ProfiledComponent - | ProfiledHook - ? (options?: NextRenderOptions) => Promise - : { error: "matcher needs to be called on a ProfiledComponent instance" }; + toRerender: T extends ( + Profiler | ProfiledComponent | ProfiledHook + ) ? + (options?: NextRenderOptions) => Promise + : { error: "matcher needs to be called on a ProfiledComponent instance" }; - toRenderExactlyTimes: T extends - | Profiler - | ProfiledComponent - | ProfiledHook - ? (count: number, options?: NextRenderOptions) => Promise - : { error: "matcher needs to be called on a ProfiledComponent instance" }; + toRenderExactlyTimes: T extends ( + Profiler | ProfiledComponent | ProfiledHook + ) ? + (count: number, options?: NextRenderOptions) => Promise + : { error: "matcher needs to be called on a ProfiledComponent instance" }; - toBeGarbageCollected: T extends WeakRef - ? () => Promise - : { error: "matcher needs to be called on a WeakRef instance" }; + toBeGarbageCollected: T extends WeakRef ? () => Promise + : { error: "matcher needs to be called on a WeakRef instance" }; } declare global { From 6d2ec4eff6dd66cd3d53df6a2ff13b504375e565 Mon Sep 17 00:00:00 2001 From: phryneas Date: Thu, 14 Dec 2023 11:40:21 +0000 Subject: [PATCH 14/14] Clean up Prettier, Size-limit, and Api-Extractor --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index fb873241928..1ac8be3319e 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38589, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32365 + "dist/apollo-client.min.cjs": 38535, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32310 }