From 0f758f7f5a0155889a58c77b0e2657abfbd3c15b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 29 Nov 2023 16:41:39 -0700 Subject: [PATCH 001/133] Initial implementation of query preloader --- src/react/index.ts | 2 + .../query-preloader/createQueryPreloader.ts | 99 +++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 src/react/query-preloader/createQueryPreloader.ts diff --git a/src/react/index.ts b/src/react/index.ts index 784046d950b..7133b368702 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -13,4 +13,6 @@ export * from "./hooks/index.js"; export type { IDocumentDefinition } from "./parser/index.js"; export { DocumentType, operationName, parser } from "./parser/index.js"; +export { createQueryPreloader } from "./query-preloader/createQueryPreloader.js"; + export * from "./types/types.js"; diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts new file mode 100644 index 00000000000..9421e036151 --- /dev/null +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -0,0 +1,99 @@ +import type { + ApolloClient, + DefaultContext, + DocumentNode, + ErrorPolicy, + FetchMoreQueryOptions, + OperationVariables, + RefetchWritePolicy, + TypedDocumentNode, + WatchQueryFetchPolicy, + WatchQueryOptions, +} from "../../core/index.js"; +import { canonicalStringify } from "../../utilities/index.js"; +import type { OnlyRequiredProperties } from "../../utilities/index.js"; +import { wrapQueryRef } from "../cache/QueryReference.js"; +import type { QueryReference } from "../cache/QueryReference.js"; +import { getSuspenseCache } from "../cache/getSuspenseCache.js"; +import type { CacheKey } from "../cache/types.js"; +import type { + FetchMoreFunction, + RefetchFunction, +} from "../hooks/useSuspenseQuery.js"; + +type VariablesOption = [ + TVariables, +] extends [never] + ? { variables?: never } + : {} extends OnlyRequiredProperties + ? { variables?: TVariables } + : { variables: TVariables }; + +export type PreloadQueryFetchPolicy = Extract< + WatchQueryFetchPolicy, + "cache-first" | "network-only" | "no-cache" | "cache-and-network" +>; + +export type PreloadQueryOptions< + TData, + TVariables extends OperationVariables, +> = { + query: DocumentNode | TypedDocumentNode; + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: PreloadQueryFetchPolicy; + queryKey?: string | number | any[]; + refetchWritePolicy?: RefetchWritePolicy; +} & VariablesOption; + +export type PreloadedQueryResult< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> = [ + QueryReference, + { + dispose: () => void; + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + }, +]; + +export function createQueryPreloader(client: ApolloClient) { + const suspenseCache = getSuspenseCache(client); + + return function preloadQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, + >( + options: PreloadQueryOptions + ): PreloadedQueryResult { + const { query, variables, queryKey, ...watchQueryOptions } = options; + + const cacheKey: CacheKey = [ + query, + canonicalStringify(variables), + ...([] as any[]).concat(queryKey), + ]; + + const queryRef = suspenseCache.getQueryRef(cacheKey, () => + client.watchQuery({ + query, + variables, + ...watchQueryOptions, + } as WatchQueryOptions) + ); + + const fetchMore: FetchMoreFunction = (options) => { + return queryRef.fetchMore(options as FetchMoreQueryOptions); + }; + + const refetch: RefetchFunction = (variables) => { + return queryRef.refetch(variables); + }; + + const dispose = queryRef.retain(); + + return [wrapQueryRef(queryRef), { fetchMore, refetch, dispose }]; + }; +} From 0f7616f7fe56c11451b715c653e7da4043298908 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 17:46:44 -0700 Subject: [PATCH 002/133] Export types from query preloader --- src/react/index.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/react/index.ts b/src/react/index.ts index 7133b368702..03f78e9cdad 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -13,6 +13,11 @@ export * from "./hooks/index.js"; export type { IDocumentDefinition } from "./parser/index.js"; export { DocumentType, operationName, parser } from "./parser/index.js"; +export type { + PreloadQueryOptions, + PreloadQueryFetchPolicy, + PreloadedQueryResult, +} from "./query-preloader/createQueryPreloader.js"; export { createQueryPreloader } from "./query-preloader/createQueryPreloader.js"; export * from "./types/types.js"; From d25d5d97f77ed59e5204ebe825e96403ab3b7767 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 17:46:55 -0700 Subject: [PATCH 003/133] Add returnPartialData option --- src/react/query-preloader/createQueryPreloader.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 9421e036151..4c2537654b3 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -44,6 +44,7 @@ export type PreloadQueryOptions< errorPolicy?: ErrorPolicy; fetchPolicy?: PreloadQueryFetchPolicy; queryKey?: string | number | any[]; + returnPartialData?: boolean; refetchWritePolicy?: RefetchWritePolicy; } & VariablesOption; From 77e11dbad3051bf58235d1b053b324989a41df8b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 18:04:19 -0700 Subject: [PATCH 004/133] Add type tests on preloadQuery for variables argument --- .../__tests__/createQueryPreloader.tsx | 160 ++++++++++++++++++ .../query-preloader/createQueryPreloader.ts | 3 +- 2 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 src/react/query-preloader/__tests__/createQueryPreloader.tsx diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.tsx new file mode 100644 index 00000000000..f05b5d567f9 --- /dev/null +++ b/src/react/query-preloader/__tests__/createQueryPreloader.tsx @@ -0,0 +1,160 @@ +import { createQueryPreloader } from "../createQueryPreloader"; +import { + ApolloClient, + InMemoryCache, + TypedDocumentNode, + gql, +} from "../../../core"; +import { MockLink } from "../../../testing"; +import { expectTypeOf } from "expect-type"; +import { QueryReference } from "../../cache/QueryReference"; + +describe.skip("type tests", () => { + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink([]), + }); + const preloadQuery = createQueryPreloader(client); + + test("returns unknown when TData cannot be inferred", () => { + const query = gql``; + + const [queryRef] = preloadQuery({ query }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + }); + + test("variables are optional and can be anything with untyped DocumentNode", () => { + const query = gql``; + + preloadQuery({ query }); + preloadQuery({ query, variables: {} }); + preloadQuery({ query, variables: { foo: "bar" } }); + preloadQuery({ query, variables: { foo: "bar", bar: 2 } }); + }); + + test("variables are optional and can be anything with unspecified TVariables on a TypedDocumentNode", () => { + const query: TypedDocumentNode<{ greeting: string }> = gql``; + + preloadQuery({ query }); + preloadQuery({ query, variables: {} }); + preloadQuery({ query, variables: { foo: "bar" } }); + preloadQuery({ query, variables: { foo: "bar", bar: 2 } }); + }); + + test("variables are optional when TVariables are empty", () => { + const query: TypedDocumentNode< + { greeting: string }, + Record + > = gql``; + + preloadQuery({ query }); + preloadQuery({ query, variables: {} }); + // @ts-expect-error unknown variables + preloadQuery({ query, variables: { foo: "bar" } }); + }); + + test("does not allow variables when TVariables is `never`", () => { + const query: TypedDocumentNode<{ greeting: string }, never> = gql``; + + preloadQuery({ query }); + // @ts-expect-error no variables option allowed + preloadQuery({ query, variables: {} }); + // @ts-expect-error no variables option allowed + preloadQuery({ query, variables: { foo: "bar" } }); + }); + + test("optional variables are optional", () => { + const query: TypedDocumentNode< + { posts: string[] }, + { limit?: number } + > = gql``; + + preloadQuery({ query }); + preloadQuery({ query, variables: {} }); + preloadQuery({ query, variables: { limit: 10 } }); + preloadQuery({ + query, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery({ + query, + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + test("enforces required variables", () => { + const query: TypedDocumentNode< + { character: string }, + { id: string } + > = gql``; + + // @ts-expect-error missing variables option + preloadQuery({ query }); + // @ts-expect-error empty variables + preloadQuery({ query, variables: {} }); + preloadQuery({ query, variables: { id: "1" } }); + preloadQuery({ + query, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery({ + query, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + test("requires variables with mixed TVariables", () => { + const query: TypedDocumentNode< + { character: string }, + { id: string; language?: string } + > = gql``; + + // @ts-expect-error missing variables argument + preloadQuery({ query }); + // @ts-expect-error missing variables argument + preloadQuery({ query, variables: {} }); + preloadQuery({ query, variables: { id: "1" } }); + // @ts-expect-error missing required variable + preloadQuery({ query, variables: { language: "en" } }); + preloadQuery({ query, variables: { id: "1", language: "en" } }); + preloadQuery({ + query, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery({ + query, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery({ + query, + variables: { + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); +}); diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 4c2537654b3..445d99a67b6 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -20,6 +20,7 @@ import type { FetchMoreFunction, RefetchFunction, } from "../hooks/useSuspenseQuery.js"; +import type { NoInfer } from "../index.js"; type VariablesOption = [ TVariables, @@ -46,7 +47,7 @@ export type PreloadQueryOptions< queryKey?: string | number | any[]; returnPartialData?: boolean; refetchWritePolicy?: RefetchWritePolicy; -} & VariablesOption; +} & VariablesOption>; export type PreloadedQueryResult< TData = unknown, From 1eb5a96d0b517d4a0b260a4dc012543c7b498cdf Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 18:42:46 -0700 Subject: [PATCH 005/133] Add function overloads to handle various options when preloading query --- .../__tests__/createQueryPreloader.tsx | 247 +++++++++++++++++- .../query-preloader/createQueryPreloader.ts | 63 ++++- 2 files changed, 299 insertions(+), 11 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.tsx index f05b5d567f9..5244a655671 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.tsx @@ -8,6 +8,11 @@ import { import { MockLink } from "../../../testing"; import { expectTypeOf } from "expect-type"; import { QueryReference } from "../../cache/QueryReference"; +import { DeepPartial } from "../../../utilities"; + +interface SimpleQueryData { + greeting: string; +} describe.skip("type tests", () => { const client = new ApolloClient({ @@ -16,14 +21,6 @@ describe.skip("type tests", () => { }); const preloadQuery = createQueryPreloader(client); - test("returns unknown when TData cannot be inferred", () => { - const query = gql``; - - const [queryRef] = preloadQuery({ query }); - - expectTypeOf(queryRef).toEqualTypeOf>(); - }); - test("variables are optional and can be anything with untyped DocumentNode", () => { const query = gql``; @@ -157,4 +154,238 @@ describe.skip("type tests", () => { }, }); }); + + test("returns QueryReference when TData cannot be inferred", () => { + const query = gql``; + + const [queryRef] = preloadQuery({ query }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + }); + + test("returns QueryReference in default case", () => { + { + const query: TypedDocumentNode = gql``; + const [queryRef] = preloadQuery({ query }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + } + + { + const query = gql``; + const [queryRef] = preloadQuery({ query }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + } + }); + + test("returns QueryReference with errorPolicy: 'ignore'", () => { + { + const query: TypedDocumentNode = gql``; + const [queryRef] = preloadQuery({ query, errorPolicy: "ignore" }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + + { + const query = gql``; + const [queryRef] = preloadQuery({ + query, + errorPolicy: "ignore", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + }); + + test("returns QueryReference with errorPolicy: 'all'", () => { + { + const query: TypedDocumentNode = gql``; + const [queryRef] = preloadQuery({ query, errorPolicy: "all" }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + + { + const query = gql``; + const [queryRef] = preloadQuery({ + query, + errorPolicy: "all", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + }); + + test("returns QueryReference with errorPolicy: 'none'", () => { + { + const query: TypedDocumentNode = gql``; + const [queryRef] = preloadQuery({ query, errorPolicy: "none" }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + } + + { + const query = gql``; + const [queryRef] = preloadQuery({ + query, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + } + }); + + test("returns QueryReference> with returnPartialData: true", () => { + { + const query: TypedDocumentNode = gql``; + const [queryRef] = preloadQuery({ query, returnPartialData: true }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference> + >(); + } + + { + const query = gql``; + const [queryRef] = preloadQuery({ + query, + returnPartialData: true, + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference> + >(); + } + }); + + test("returns QueryReference> with returnPartialData: false", () => { + { + const query: TypedDocumentNode = gql``; + const [queryRef] = preloadQuery({ query, returnPartialData: false }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + } + + { + const query = gql``; + const [queryRef] = preloadQuery({ + query, + returnPartialData: false, + }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + } + }); + + test("returns QueryReference when passing an option unrelated to TData", () => { + { + const query: TypedDocumentNode = gql``; + const [queryRef] = preloadQuery({ query, canonizeResults: true }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + } + + { + const query = gql``; + const [queryRef] = preloadQuery({ + query, + canonizeResults: true, + }); + + expectTypeOf(queryRef).toEqualTypeOf>(); + } + }); + + test("handles combinations of options", () => { + { + const query: TypedDocumentNode = gql``; + const [queryRef] = preloadQuery({ + query, + returnPartialData: true, + errorPolicy: "ignore", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference | undefined> + >(); + } + + { + const query = gql``; + const [queryRef] = preloadQuery({ + query, + returnPartialData: true, + errorPolicy: "ignore", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference | undefined> + >(); + } + + { + const query: TypedDocumentNode = gql``; + const [queryRef] = preloadQuery({ + query, + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference> + >(); + } + + { + const query = gql``; + const [queryRef] = preloadQuery({ + query, + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference> + >(); + } + }); + + test("returns correct TData type when combined with options unrelated to TData", () => { + { + const query: TypedDocumentNode = gql``; + const [queryRef] = preloadQuery({ + query, + canonizeResults: true, + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference> + >(); + } + + { + const query = gql``; + const [queryRef] = preloadQuery({ + query, + canonizeResults: true, + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference> + >(); + } + }); }); diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 445d99a67b6..f4ff5ef6d95 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -11,7 +11,10 @@ import type { WatchQueryOptions, } from "../../core/index.js"; import { canonicalStringify } from "../../utilities/index.js"; -import type { OnlyRequiredProperties } from "../../utilities/index.js"; +import type { + DeepPartial, + OnlyRequiredProperties, +} from "../../utilities/index.js"; import { wrapQueryRef } from "../cache/QueryReference.js"; import type { QueryReference } from "../cache/QueryReference.js"; import { getSuspenseCache } from "../cache/getSuspenseCache.js"; @@ -64,7 +67,59 @@ export type PreloadedQueryResult< export function createQueryPreloader(client: ApolloClient) { const suspenseCache = getSuspenseCache(client); - return function preloadQuery< + function preloadQuery< + TData, + TVariables extends OperationVariables, + TOptions extends PreloadQueryOptions, + >( + options: PreloadQueryOptions & TOptions + ): PreloadedQueryResult< + TOptions["errorPolicy"] extends "ignore" | "all" + ? TOptions["returnPartialData"] extends true + ? DeepPartial | undefined + : TData | undefined + : TOptions["returnPartialData"] extends true + ? DeepPartial + : TData, + TVariables + >; + + function preloadQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, + >( + options: PreloadQueryOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + } + ): PreloadedQueryResult | undefined, TVariables>; + + function preloadQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, + >( + options: PreloadQueryOptions & { + errorPolicy: "ignore" | "all"; + } + ): PreloadedQueryResult; + + function preloadQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, + >( + options: PreloadQueryOptions & { + returnPartialData: true; + } + ): PreloadedQueryResult, TVariables>; + + function preloadQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, + >( + options: PreloadQueryOptions + ): PreloadedQueryResult; + + function preloadQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables, >( @@ -97,5 +152,7 @@ export function createQueryPreloader(client: ApolloClient) { const dispose = queryRef.retain(); return [wrapQueryRef(queryRef), { fetchMore, refetch, dispose }]; - }; + } + + return preloadQuery; } From dd2523942184a2212939d05c21721f15ca127c36 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 18:44:41 -0700 Subject: [PATCH 006/133] Add .test.tsx suffix to test file --- .../{createQueryPreloader.tsx => createQueryPreloader.test.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/react/query-preloader/__tests__/{createQueryPreloader.tsx => createQueryPreloader.test.tsx} (100%) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx similarity index 100% rename from src/react/query-preloader/__tests__/createQueryPreloader.tsx rename to src/react/query-preloader/__tests__/createQueryPreloader.test.tsx From 8300ddc990c9ed313e6a3c283fd6079fe1d28fd8 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 18:48:19 -0700 Subject: [PATCH 007/133] Only run query preloader tests with React 18 --- config/jest.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/config/jest.config.js b/config/jest.config.js index a45df96fc48..de17e13a60f 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -36,6 +36,7 @@ const react17TestFileIgnoreList = [ "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", "src/react/hooks/__tests__/useLoadableQuery.test.tsx", + "src/react/query-preloader/__tests__/createQueryPreloader.test.tsx", ]; const tsStandardConfig = { From 8cc96d74c48badfb17c561ae40d19f05db8e196a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 18:48:58 -0700 Subject: [PATCH 008/133] Fix regression in variables type --- src/react/query-preloader/createQueryPreloader.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index f4ff5ef6d95..671164823f3 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -72,7 +72,8 @@ export function createQueryPreloader(client: ApolloClient) { TVariables extends OperationVariables, TOptions extends PreloadQueryOptions, >( - options: PreloadQueryOptions & TOptions + options: PreloadQueryOptions & + Omit ): PreloadedQueryResult< TOptions["errorPolicy"] extends "ignore" | "all" ? TOptions["returnPartialData"] extends true From cf758b29360cb7d2f7565d7957c5a70257b7b815 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 22:13:01 -0700 Subject: [PATCH 009/133] Pass query as first arg and options as second --- .../__tests__/createQueryPreloader.test.tsx | 125 ++++++++---------- .../query-preloader/createQueryPreloader.ts | 40 ++++-- 2 files changed, 80 insertions(+), 85 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 5244a655671..c313654f617 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -24,19 +24,19 @@ describe.skip("type tests", () => { test("variables are optional and can be anything with untyped DocumentNode", () => { const query = gql``; - preloadQuery({ query }); - preloadQuery({ query, variables: {} }); - preloadQuery({ query, variables: { foo: "bar" } }); - preloadQuery({ query, variables: { foo: "bar", bar: 2 } }); + preloadQuery(query); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: { foo: "bar" } }); + preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); }); test("variables are optional and can be anything with unspecified TVariables on a TypedDocumentNode", () => { const query: TypedDocumentNode<{ greeting: string }> = gql``; - preloadQuery({ query }); - preloadQuery({ query, variables: {} }); - preloadQuery({ query, variables: { foo: "bar" } }); - preloadQuery({ query, variables: { foo: "bar", bar: 2 } }); + preloadQuery(query); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: { foo: "bar" } }); + preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); }); test("variables are optional when TVariables are empty", () => { @@ -45,20 +45,20 @@ describe.skip("type tests", () => { Record > = gql``; - preloadQuery({ query }); - preloadQuery({ query, variables: {} }); + preloadQuery(query); + preloadQuery(query, { variables: {} }); // @ts-expect-error unknown variables - preloadQuery({ query, variables: { foo: "bar" } }); + preloadQuery(query, { variables: { foo: "bar" } }); }); test("does not allow variables when TVariables is `never`", () => { const query: TypedDocumentNode<{ greeting: string }, never> = gql``; - preloadQuery({ query }); + preloadQuery(query); // @ts-expect-error no variables option allowed - preloadQuery({ query, variables: {} }); + preloadQuery(query, { variables: {} }); // @ts-expect-error no variables option allowed - preloadQuery({ query, variables: { foo: "bar" } }); + preloadQuery(query, { variables: { foo: "bar" } }); }); test("optional variables are optional", () => { @@ -67,18 +67,16 @@ describe.skip("type tests", () => { { limit?: number } > = gql``; - preloadQuery({ query }); - preloadQuery({ query, variables: {} }); - preloadQuery({ query, variables: { limit: 10 } }); - preloadQuery({ - query, + preloadQuery(query); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: { limit: 10 } }); + preloadQuery(query, { variables: { // @ts-expect-error unknown variable foo: "bar", }, }); - preloadQuery({ - query, + preloadQuery(query, { variables: { limit: 10, // @ts-expect-error unknown variable @@ -94,19 +92,17 @@ describe.skip("type tests", () => { > = gql``; // @ts-expect-error missing variables option - preloadQuery({ query }); + preloadQuery(query); // @ts-expect-error empty variables - preloadQuery({ query, variables: {} }); - preloadQuery({ query, variables: { id: "1" } }); - preloadQuery({ - query, + preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: { id: "1" } }); + preloadQuery(query, { variables: { // @ts-expect-error unknown variable foo: "bar", }, }); - preloadQuery({ - query, + preloadQuery(query, { variables: { id: "1", // @ts-expect-error unknown variable @@ -122,30 +118,27 @@ describe.skip("type tests", () => { > = gql``; // @ts-expect-error missing variables argument - preloadQuery({ query }); + preloadQuery(query); // @ts-expect-error missing variables argument - preloadQuery({ query, variables: {} }); - preloadQuery({ query, variables: { id: "1" } }); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: { id: "1" } }); // @ts-expect-error missing required variable - preloadQuery({ query, variables: { language: "en" } }); - preloadQuery({ query, variables: { id: "1", language: "en" } }); - preloadQuery({ - query, + preloadQuery(query, { variables: { language: "en" } }); + preloadQuery(query, { variables: { id: "1", language: "en" } }); + preloadQuery(query, { variables: { // @ts-expect-error unknown variable foo: "bar", }, }); - preloadQuery({ - query, + preloadQuery(query, { variables: { id: "1", // @ts-expect-error unknown variable foo: "bar", }, }); - preloadQuery({ - query, + preloadQuery(query, { variables: { id: "1", language: "en", @@ -158,7 +151,7 @@ describe.skip("type tests", () => { test("returns QueryReference when TData cannot be inferred", () => { const query = gql``; - const [queryRef] = preloadQuery({ query }); + const [queryRef] = preloadQuery(query); expectTypeOf(queryRef).toEqualTypeOf>(); }); @@ -166,14 +159,14 @@ describe.skip("type tests", () => { test("returns QueryReference in default case", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery({ query }); + const [queryRef] = preloadQuery(query); expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery({ query }); + const [queryRef] = preloadQuery(query); expectTypeOf(queryRef).toEqualTypeOf>(); } @@ -182,7 +175,7 @@ describe.skip("type tests", () => { test("returns QueryReference with errorPolicy: 'ignore'", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery({ query, errorPolicy: "ignore" }); + const [queryRef] = preloadQuery(query, { errorPolicy: "ignore" }); expectTypeOf(queryRef).toEqualTypeOf< QueryReference @@ -191,8 +184,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { errorPolicy: "ignore", }); @@ -205,7 +197,7 @@ describe.skip("type tests", () => { test("returns QueryReference with errorPolicy: 'all'", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery({ query, errorPolicy: "all" }); + const [queryRef] = preloadQuery(query, { errorPolicy: "all" }); expectTypeOf(queryRef).toEqualTypeOf< QueryReference @@ -214,8 +206,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { errorPolicy: "all", }); @@ -228,15 +219,14 @@ describe.skip("type tests", () => { test("returns QueryReference with errorPolicy: 'none'", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery({ query, errorPolicy: "none" }); + const [queryRef] = preloadQuery(query, { errorPolicy: "none" }); expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { errorPolicy: "none", }); @@ -247,7 +237,7 @@ describe.skip("type tests", () => { test("returns QueryReference> with returnPartialData: true", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery({ query, returnPartialData: true }); + const [queryRef] = preloadQuery(query, { returnPartialData: true }); expectTypeOf(queryRef).toEqualTypeOf< QueryReference> @@ -256,8 +246,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { returnPartialData: true, }); @@ -270,15 +259,14 @@ describe.skip("type tests", () => { test("returns QueryReference> with returnPartialData: false", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery({ query, returnPartialData: false }); + const [queryRef] = preloadQuery(query, { returnPartialData: false }); expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { returnPartialData: false, }); @@ -289,15 +277,14 @@ describe.skip("type tests", () => { test("returns QueryReference when passing an option unrelated to TData", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery({ query, canonizeResults: true }); + const [queryRef] = preloadQuery(query, { canonizeResults: true }); expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { canonizeResults: true, }); @@ -308,8 +295,7 @@ describe.skip("type tests", () => { test("handles combinations of options", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { returnPartialData: true, errorPolicy: "ignore", }); @@ -321,8 +307,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { returnPartialData: true, errorPolicy: "ignore", }); @@ -334,8 +319,7 @@ describe.skip("type tests", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { returnPartialData: true, errorPolicy: "none", }); @@ -347,8 +331,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { returnPartialData: true, errorPolicy: "none", }); @@ -362,8 +345,7 @@ describe.skip("type tests", () => { test("returns correct TData type when combined with options unrelated to TData", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { canonizeResults: true, returnPartialData: true, errorPolicy: "none", @@ -376,8 +358,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery({ - query, + const [queryRef] = preloadQuery(query, { canonizeResults: true, returnPartialData: true, errorPolicy: "none", diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 671164823f3..a6f0ec39387 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -39,10 +39,8 @@ export type PreloadQueryFetchPolicy = Extract< >; export type PreloadQueryOptions< - TData, - TVariables extends OperationVariables, + TVariables extends OperationVariables = OperationVariables, > = { - query: DocumentNode | TypedDocumentNode; canonizeResults?: boolean; context?: DefaultContext; errorPolicy?: ErrorPolicy; @@ -50,7 +48,7 @@ export type PreloadQueryOptions< queryKey?: string | number | any[]; returnPartialData?: boolean; refetchWritePolicy?: RefetchWritePolicy; -} & VariablesOption>; +} & VariablesOption; export type PreloadedQueryResult< TData = unknown, @@ -64,16 +62,25 @@ export type PreloadedQueryResult< }, ]; +type PreloadQueryOptionsArg< + TVariables extends OperationVariables, + TOptions = unknown, +> = [TVariables] extends [never] + ? [options?: PreloadQueryOptions & TOptions] + : {} extends OnlyRequiredProperties + ? [options?: PreloadQueryOptions> & TOptions] + : [options: PreloadQueryOptions> & TOptions]; + export function createQueryPreloader(client: ApolloClient) { const suspenseCache = getSuspenseCache(client); function preloadQuery< TData, TVariables extends OperationVariables, - TOptions extends PreloadQueryOptions, + TOptions extends Omit, "variables">, >( - options: PreloadQueryOptions & - Omit + query: DocumentNode | TypedDocumentNode, + ...[options]: PreloadQueryOptionsArg, TOptions> ): PreloadedQueryResult< TOptions["errorPolicy"] extends "ignore" | "all" ? TOptions["returnPartialData"] extends true @@ -89,7 +96,8 @@ export function createQueryPreloader(client: ApolloClient) { TData = unknown, TVariables extends OperationVariables = OperationVariables, >( - options: PreloadQueryOptions & { + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions & { returnPartialData: true; errorPolicy: "ignore" | "all"; } @@ -99,7 +107,8 @@ export function createQueryPreloader(client: ApolloClient) { TData = unknown, TVariables extends OperationVariables = OperationVariables, >( - options: PreloadQueryOptions & { + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & { errorPolicy: "ignore" | "all"; } ): PreloadedQueryResult; @@ -108,7 +117,8 @@ export function createQueryPreloader(client: ApolloClient) { TData = unknown, TVariables extends OperationVariables = OperationVariables, >( - options: PreloadQueryOptions & { + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions & { returnPartialData: true; } ): PreloadedQueryResult, TVariables>; @@ -117,16 +127,20 @@ export function createQueryPreloader(client: ApolloClient) { TData = unknown, TVariables extends OperationVariables = OperationVariables, >( - options: PreloadQueryOptions + query: DocumentNode | TypedDocumentNode, + ...[options]: PreloadQueryOptionsArg> ): PreloadedQueryResult; function preloadQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables, >( - options: PreloadQueryOptions + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions & VariablesOption = Object.create( + null + ) ): PreloadedQueryResult { - const { query, variables, queryKey, ...watchQueryOptions } = options; + const { variables, queryKey, ...watchQueryOptions } = options; const cacheKey: CacheKey = [ query, From 165e0ef148f9acfff85eb587af1e893778ebdd28 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 22:33:09 -0700 Subject: [PATCH 010/133] Create prebuilt scenarios that can be used for test cases --- src/testing/internal/index.ts | 3 ++ src/testing/internal/scenarios/index.ts | 60 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/testing/internal/scenarios/index.ts diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index 73a9a00ff0e..92dfafd76e9 100644 --- a/src/testing/internal/index.ts +++ b/src/testing/internal/index.ts @@ -1,3 +1,6 @@ export * from "./profile/index.js"; export * from "./disposables/index.js"; export { ObservableStream } from "./ObservableStream.js"; + +export type { SimpleCaseData, VariablesCaseData } from "./scenarios/index.js"; +export { useSimpleCase, useVariablesCase } from "./scenarios/index.js"; diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts new file mode 100644 index 00000000000..6ef12fde436 --- /dev/null +++ b/src/testing/internal/scenarios/index.ts @@ -0,0 +1,60 @@ +import { TypedDocumentNode, gql } from "../../../core/index.js"; +import { MockedResponse } from "../../core/index.js"; + +export interface SimpleCaseData { + greeting: string; +} + +export function useSimpleCase() { + const query: TypedDocumentNode = gql` + query GreetingQuery { + greeting + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + delay: 10, + }, + ]; + + return { query, mocks }; +} + +export interface VariablesCaseData { + character: { + id: string; + name: string; + }; +} + +export interface VariablesCaseVariables { + id: string; +} + +export function useVariablesCase() { + const query: TypedDocumentNode< + VariablesCaseData, + VariablesCaseVariables + > = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } + } + `; + const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"]; + + const mocks: MockedResponse[] = [...CHARACTERS].map( + (name, index) => ({ + request: { query, variables: { id: String(index + 1) } }, + result: { data: { character: { id: String(index + 1), name } } }, + delay: 20, + }) + ); + + return { mocks, query }; +} From bc1ac5da64fdecd406f40459929595dba9f12953 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 22:47:56 -0700 Subject: [PATCH 011/133] Write first test for loading a query outside react --- .../__tests__/createQueryPreloader.test.tsx | 90 ++++++++++++++++++- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index c313654f617..2be881f7f38 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -1,19 +1,103 @@ +import React, { Suspense } from "react"; +import type { ReactElement } from "react"; import { createQueryPreloader } from "../createQueryPreloader"; import { ApolloClient, InMemoryCache, + NetworkStatus, TypedDocumentNode, gql, } from "../../../core"; -import { MockLink } from "../../../testing"; +import { MockLink, MockedResponse } from "../../../testing"; import { expectTypeOf } from "expect-type"; import { QueryReference } from "../../cache/QueryReference"; import { DeepPartial } from "../../../utilities"; +import { + SimpleCaseData, + createProfiler, + useSimpleCase, + useTrackRenders, +} from "../../../testing/internal"; +import { ApolloProvider } from "../../context"; +import { RenderOptions, render } from "@testing-library/react"; +import { UseReadQueryResult, useReadQuery } from "../../hooks"; + +function createDefaultClient(mocks: MockedResponse[]) { + return new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); +} -interface SimpleQueryData { - greeting: string; +function renderWithClient( + ui: ReactElement, + { + client, + wrapper: Wrapper = React.Fragment, + }: { client: ApolloClient; wrapper?: RenderOptions["wrapper"] } +) { + return render(ui, { + wrapper: ({ children }) => ( + + {children} + + ), + }); } +test("loads a query and suspends when passed to useReadQuery", async () => { + const { query, mocks } = useSimpleCase(); + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const [queryRef, { dispose }] = preloadQuery(query); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + return ( + }> + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + dispose(); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From 76426c40282f7e664b99b4844a39f9c1a667c40e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 22:48:10 -0700 Subject: [PATCH 012/133] Fix type name in type tests --- .../__tests__/createQueryPreloader.test.tsx | 80 +++++++++---------- 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 2be881f7f38..b3e3dd52b6f 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -242,193 +242,193 @@ describe.skip("type tests", () => { test("returns QueryReference in default case", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const [queryRef] = preloadQuery(query); - expectTypeOf(queryRef).toEqualTypeOf>(); + expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery(query); + const [queryRef] = preloadQuery(query); - expectTypeOf(queryRef).toEqualTypeOf>(); + expectTypeOf(queryRef).toEqualTypeOf>(); } }); test("returns QueryReference with errorPolicy: 'ignore'", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const [queryRef] = preloadQuery(query, { errorPolicy: "ignore" }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference + QueryReference >(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const [queryRef] = preloadQuery(query, { errorPolicy: "ignore", }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference + QueryReference >(); } }); test("returns QueryReference with errorPolicy: 'all'", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const [queryRef] = preloadQuery(query, { errorPolicy: "all" }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference + QueryReference >(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const [queryRef] = preloadQuery(query, { errorPolicy: "all", }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference + QueryReference >(); } }); test("returns QueryReference with errorPolicy: 'none'", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const [queryRef] = preloadQuery(query, { errorPolicy: "none" }); - expectTypeOf(queryRef).toEqualTypeOf>(); + expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const [queryRef] = preloadQuery(query, { errorPolicy: "none", }); - expectTypeOf(queryRef).toEqualTypeOf>(); + expectTypeOf(queryRef).toEqualTypeOf>(); } }); test("returns QueryReference> with returnPartialData: true", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const [queryRef] = preloadQuery(query, { returnPartialData: true }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference> + QueryReference> >(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const [queryRef] = preloadQuery(query, { returnPartialData: true, }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference> + QueryReference> >(); } }); test("returns QueryReference> with returnPartialData: false", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const [queryRef] = preloadQuery(query, { returnPartialData: false }); - expectTypeOf(queryRef).toEqualTypeOf>(); + expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const [queryRef] = preloadQuery(query, { returnPartialData: false, }); - expectTypeOf(queryRef).toEqualTypeOf>(); + expectTypeOf(queryRef).toEqualTypeOf>(); } }); test("returns QueryReference when passing an option unrelated to TData", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const [queryRef] = preloadQuery(query, { canonizeResults: true }); - expectTypeOf(queryRef).toEqualTypeOf>(); + expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const [queryRef] = preloadQuery(query, { canonizeResults: true, }); - expectTypeOf(queryRef).toEqualTypeOf>(); + expectTypeOf(queryRef).toEqualTypeOf>(); } }); test("handles combinations of options", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const [queryRef] = preloadQuery(query, { returnPartialData: true, errorPolicy: "ignore", }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference | undefined> + QueryReference | undefined> >(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const [queryRef] = preloadQuery(query, { returnPartialData: true, errorPolicy: "ignore", }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference | undefined> + QueryReference | undefined> >(); } { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const [queryRef] = preloadQuery(query, { returnPartialData: true, errorPolicy: "none", }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference> + QueryReference> >(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const [queryRef] = preloadQuery(query, { returnPartialData: true, errorPolicy: "none", }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference> + QueryReference> >(); } }); test("returns correct TData type when combined with options unrelated to TData", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const [queryRef] = preloadQuery(query, { canonizeResults: true, returnPartialData: true, @@ -436,20 +436,20 @@ describe.skip("type tests", () => { }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference> + QueryReference> >(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const [queryRef] = preloadQuery(query, { canonizeResults: true, returnPartialData: true, errorPolicy: "none", }); expectTypeOf(queryRef).toEqualTypeOf< - QueryReference> + QueryReference> >(); } }); From cc9c88cb093ce8ca565f814894d0c7e0c194c11d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 30 Nov 2023 22:57:15 -0700 Subject: [PATCH 013/133] Add another test that checks loading a query with variables --- .../__tests__/createQueryPreloader.test.tsx | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index b3e3dd52b6f..25bf71a19a9 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -14,9 +14,11 @@ import { QueryReference } from "../../cache/QueryReference"; import { DeepPartial } from "../../../utilities"; import { SimpleCaseData, + VariablesCaseData, createProfiler, useSimpleCase, useTrackRenders, + useVariablesCase, } from "../../../testing/internal"; import { ApolloProvider } from "../../context"; import { RenderOptions, render } from "@testing-library/react"; @@ -98,6 +100,61 @@ test("loads a query and suspends when passed to useReadQuery", async () => { dispose(); }); +test("loads a query with variables and suspends when passed to useReadQuery", async () => { + const { query, mocks } = useVariablesCase(); + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const [queryRef, { dispose }] = preloadQuery(query, { + variables: { id: "1" }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + return ( + }> + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + dispose(); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From d72087e20d95bb55748be70e49038f339f730f30 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 10:55:07 -0700 Subject: [PATCH 014/133] Return dispose as 2nd tuple item and remove refetch/fetchMore --- .../__tests__/createQueryPreloader.test.tsx | 4 ++-- .../query-preloader/createQueryPreloader.ts | 24 ++----------------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 25bf71a19a9..bd44de0aacd 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -57,7 +57,7 @@ test("loads a query and suspends when passed to useReadQuery", async () => { }, }); - const [queryRef, { dispose }] = preloadQuery(query); + const [queryRef, dispose] = preloadQuery(query); function SuspenseFallback() { useTrackRenders(); @@ -110,7 +110,7 @@ test("loads a query with variables and suspends when passed to useReadQuery", as }, }); - const [queryRef, { dispose }] = preloadQuery(query, { + const [queryRef, dispose] = preloadQuery(query, { variables: { id: "1" }, }); diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index a6f0ec39387..d514752dc95 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -3,7 +3,6 @@ import type { DefaultContext, DocumentNode, ErrorPolicy, - FetchMoreQueryOptions, OperationVariables, RefetchWritePolicy, TypedDocumentNode, @@ -19,10 +18,6 @@ import { wrapQueryRef } from "../cache/QueryReference.js"; import type { QueryReference } from "../cache/QueryReference.js"; import { getSuspenseCache } from "../cache/getSuspenseCache.js"; import type { CacheKey } from "../cache/types.js"; -import type { - FetchMoreFunction, - RefetchFunction, -} from "../hooks/useSuspenseQuery.js"; import type { NoInfer } from "../index.js"; type VariablesOption = [ @@ -53,14 +48,7 @@ export type PreloadQueryOptions< export type PreloadedQueryResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, -> = [ - QueryReference, - { - dispose: () => void; - fetchMore: FetchMoreFunction; - refetch: RefetchFunction; - }, -]; +> = [QueryReference, dispose: () => void]; type PreloadQueryOptionsArg< TVariables extends OperationVariables, @@ -156,17 +144,9 @@ export function createQueryPreloader(client: ApolloClient) { } as WatchQueryOptions) ); - const fetchMore: FetchMoreFunction = (options) => { - return queryRef.fetchMore(options as FetchMoreQueryOptions); - }; - - const refetch: RefetchFunction = (variables) => { - return queryRef.refetch(variables); - }; - const dispose = queryRef.retain(); - return [wrapQueryRef(queryRef), { fetchMore, refetch, dispose }]; + return [wrapQueryRef(queryRef), dispose]; } return preloadQuery; From ca46ed42d5bf3a648d676e2f5c7646a18b876d0e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 10:58:32 -0700 Subject: [PATCH 015/133] Fix incorrect expectation in test --- .../query-preloader/__tests__/createQueryPreloader.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index bd44de0aacd..86275d70121 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -91,7 +91,7 @@ test("loads a query and suspends when passed to useReadQuery", async () => { const { snapshot } = await Profiler.takeRender(); expect(snapshot.result).toEqual({ - data: { greeting: "hello" }, + data: { greeting: "Hello" }, error: undefined, networkStatus: NetworkStatus.ready, }); From 0739ceaa7ac511a08fd901bebe25a22878d09f76 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 10:59:03 -0700 Subject: [PATCH 016/133] Run prettier --- .../__tests__/createQueryPreloader.test.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 86275d70121..8d7b1bf4ac6 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -203,10 +203,8 @@ describe.skip("type tests", () => { }); test("optional variables are optional", () => { - const query: TypedDocumentNode< - { posts: string[] }, - { limit?: number } - > = gql``; + const query: TypedDocumentNode<{ posts: string[] }, { limit?: number }> = + gql``; preloadQuery(query); preloadQuery(query, { variables: {} }); @@ -227,10 +225,8 @@ describe.skip("type tests", () => { }); test("enforces required variables", () => { - const query: TypedDocumentNode< - { character: string }, - { id: string } - > = gql``; + const query: TypedDocumentNode<{ character: string }, { id: string }> = + gql``; // @ts-expect-error missing variables option preloadQuery(query); From e01f9399547c047bad93ea40010d01cc4368a21b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 14:40:27 -0700 Subject: [PATCH 017/133] Run prettier on createQueryPreloader --- .../query-preloader/createQueryPreloader.ts | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index d514752dc95..186db3e54cd 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -20,12 +20,9 @@ import { getSuspenseCache } from "../cache/getSuspenseCache.js"; import type { CacheKey } from "../cache/types.js"; import type { NoInfer } from "../index.js"; -type VariablesOption = [ - TVariables, -] extends [never] - ? { variables?: never } - : {} extends OnlyRequiredProperties - ? { variables?: TVariables } +type VariablesOption = + [TVariables] extends [never] ? { variables?: never } + : {} extends OnlyRequiredProperties ? { variables?: TVariables } : { variables: TVariables }; export type PreloadQueryFetchPolicy = Extract< @@ -53,11 +50,11 @@ export type PreloadedQueryResult< type PreloadQueryOptionsArg< TVariables extends OperationVariables, TOptions = unknown, -> = [TVariables] extends [never] - ? [options?: PreloadQueryOptions & TOptions] - : {} extends OnlyRequiredProperties - ? [options?: PreloadQueryOptions> & TOptions] - : [options: PreloadQueryOptions> & TOptions]; +> = [TVariables] extends [never] ? + [options?: PreloadQueryOptions & TOptions] +: {} extends OnlyRequiredProperties ? + [options?: PreloadQueryOptions> & TOptions] +: [options: PreloadQueryOptions> & TOptions]; export function createQueryPreloader(client: ApolloClient) { const suspenseCache = getSuspenseCache(client); @@ -70,13 +67,12 @@ export function createQueryPreloader(client: ApolloClient) { query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg, TOptions> ): PreloadedQueryResult< - TOptions["errorPolicy"] extends "ignore" | "all" - ? TOptions["returnPartialData"] extends true - ? DeepPartial | undefined - : TData | undefined - : TOptions["returnPartialData"] extends true - ? DeepPartial - : TData, + TOptions["errorPolicy"] extends "ignore" | "all" ? + TOptions["returnPartialData"] extends true ? + DeepPartial | undefined + : TData | undefined + : TOptions["returnPartialData"] extends true ? DeepPartial + : TData, TVariables >; From ff3a6b5bc2d370a1370ea664bcdb163fa8e2d38b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 14:40:40 -0700 Subject: [PATCH 018/133] Default to empty queryKey in preloadQuery --- src/react/query-preloader/createQueryPreloader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 186db3e54cd..0861e43e0dd 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -124,7 +124,7 @@ export function createQueryPreloader(client: ApolloClient) { null ) ): PreloadedQueryResult { - const { variables, queryKey, ...watchQueryOptions } = options; + const { variables, queryKey = [], ...watchQueryOptions } = options; const cacheKey: CacheKey = [ query, From bbea69da837401a84a24bcdb45056cfb02d0ea1e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 14:41:17 -0700 Subject: [PATCH 019/133] Add test to ensure calling dispose will dispose of the query --- .../__tests__/createQueryPreloader.test.tsx | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 8d7b1bf4ac6..dd6d4cd1d83 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -8,7 +8,7 @@ import { TypedDocumentNode, gql, } from "../../../core"; -import { MockLink, MockedResponse } from "../../../testing"; +import { MockLink, MockedResponse, wait } from "../../../testing"; import { expectTypeOf } from "expect-type"; import { QueryReference } from "../../cache/QueryReference"; import { DeepPartial } from "../../../utilities"; @@ -155,6 +155,24 @@ test("loads a query with variables and suspends when passed to useReadQuery", as dispose(); }); +test("tears down the query when calling dispose", async () => { + const { query, mocks } = useSimpleCase(); + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const [, dispose] = preloadQuery(query); + + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + dispose(); + + await wait(0); + + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From 748cfb85f15999fbf81d2e49294a21dedee9957e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 18:02:07 -0700 Subject: [PATCH 020/133] Add ability to determine if the query ref has been disposed --- src/react/cache/QueryReference.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 97025c37335..ee097944677 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -103,6 +103,7 @@ export class InternalQueryReference { private listeners = new Set>(); private autoDisposeTimeoutId?: NodeJS.Timeout; private status: "idle" | "loading" = "loading"; + private __disposed = false; private resolve: ((result: ApolloQueryResult) => void) | undefined; private reject: ((error: unknown) => void) | undefined; @@ -171,6 +172,10 @@ export class InternalQueryReference { return this.observable.options; } + get disposed() { + return this.__disposed; + } + retain() { this.references++; clearTimeout(this.autoDisposeTimeoutId); @@ -242,6 +247,7 @@ export class InternalQueryReference { } private dispose() { + this.__disposed = true; this.subscription.unsubscribe(); this.onDispose(); } From 7b913e717795a368743b8b28609c55d06e9863c2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 18:02:30 -0700 Subject: [PATCH 021/133] Warn if using disposed query ref in useReadQuery --- src/react/hooks/useReadQuery.ts | 15 +++++ .../__tests__/createQueryPreloader.test.tsx | 61 +++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index f2320aa58ea..c61f81e6069 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -9,6 +9,7 @@ import { toApolloError } from "./useSuspenseQuery.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import type { ApolloError } from "../../errors/index.js"; import type { NetworkStatus } from "../../core/index.js"; +import { invariant } from "../../utilities/globals/index.js"; export interface UseReadQueryResult { /** @@ -38,11 +39,25 @@ export interface UseReadQueryResult { export function useReadQuery( queryRef: QueryReference ): UseReadQueryResult { + const didWarnOnDisposedQueryRef = React.useRef(false); const [internalQueryRef, getPromise] = React.useMemo( () => unwrapQueryRef(queryRef), [queryRef] ); + if (!didWarnOnDisposedQueryRef.current && internalQueryRef.disposed) { + invariant.warn( + ` +'useReadQuery' was called with a disposed queryRef which means the query is no longer watched and cache updates will be missed. + +This occurs when calling 'dispose' while 'useReadQuery' is still mounted, either by calling 'dispose' too early, or because you are using React's strict mode and calling 'dispose' in a 'useEffect' cleanup function. + +If you're using a queryRef produced by 'useBackgroundQuery' or 'useLoadableQuery', this is a bug in Apollo Client. Please file an issue. +` + ); + didWarnOnDisposedQueryRef.current = true; + } + const promise = useSyncExternalStore( React.useCallback( (forceUpdate) => { diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index dd6d4cd1d83..0038dedea1f 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -16,6 +16,7 @@ import { SimpleCaseData, VariablesCaseData, createProfiler, + spyOnConsole, useSimpleCase, useTrackRenders, useVariablesCase, @@ -173,6 +174,66 @@ test("tears down the query when calling dispose", async () => { expect(client).not.toHaveSuspenseCacheEntryUsing(query); }); +test("useReadQuery warns when called with a disposed queryRef", async () => { + using _consoleSpy = spyOnConsole("warn"); + const { query, mocks } = useSimpleCase(); + const client = createDefaultClient(mocks); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + return ( + }> + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + const { rerender } = renderWithClient(, { client, wrapper: Profiler }); + + await Profiler.takeRender(); + await Profiler.takeRender(); + + await expect(Profiler).not.toRerender(); + + dispose(); + + await wait(0); + + rerender(); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining( + "'useReadQuery' was called with a disposed queryRef" + ) + ); + + rerender(); + + // Ensure re-rendering again only shows the warning once + expect(console.warn).toHaveBeenCalledTimes(1); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From e80697bad3c71862e4f7b3e45e6b922d56300309 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 18:26:25 -0700 Subject: [PATCH 022/133] Add test to ensure cache updates are handled --- .../__tests__/createQueryPreloader.test.tsx | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 0038dedea1f..636eb66848f 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -234,6 +234,74 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { expect(console.warn).toHaveBeenCalledTimes(1); }); +test("can handle cache updates", async () => { + const { query, mocks } = useSimpleCase(); + const client = createDefaultClient(mocks); + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + return ( + }> + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ + query, + data: { greeting: "Hello (updated)" }, + }); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello (updated)" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + dispose(); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From c24075c569ff8741de24e4f560f0643df0649c7e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 18:31:39 -0700 Subject: [PATCH 023/133] Add test to ensure context is passed to the link --- .../__tests__/createQueryPreloader.test.tsx | 74 ++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 636eb66848f..6ac16fbc30e 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -3,6 +3,7 @@ import type { ReactElement } from "react"; import { createQueryPreloader } from "../createQueryPreloader"; import { ApolloClient, + ApolloLink, InMemoryCache, NetworkStatus, TypedDocumentNode, @@ -11,7 +12,7 @@ import { import { MockLink, MockedResponse, wait } from "../../../testing"; import { expectTypeOf } from "expect-type"; import { QueryReference } from "../../cache/QueryReference"; -import { DeepPartial } from "../../../utilities"; +import { DeepPartial, Observable } from "../../../utilities"; import { SimpleCaseData, VariablesCaseData, @@ -302,6 +303,77 @@ test("can handle cache updates", async () => { dispose(); }); +test("passes context to the link", async () => { + interface QueryData { + context: Record; + } + + const query: TypedDocumentNode = gql` + query ContextQuery { + context + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink((operation) => { + return new Observable((observer) => { + const { valueA, valueB } = operation.getContext(); + setTimeout(() => { + observer.next({ data: { context: { valueA, valueB } } }); + observer.complete(); + }, 10); + }); + }), + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { + context: { valueA: "A", valueB: "B" }, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + return ( + }> + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { context: { valueA: "A", valueB: "B" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + + dispose(); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From eb0bd84a475e332eb631a31d3589d9f83585c7f4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 18:41:14 -0700 Subject: [PATCH 024/133] Add test to ensure errors are thrown and error boundary is shown --- .../__tests__/createQueryPreloader.test.tsx | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 6ac16fbc30e..613581fdba6 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -3,6 +3,7 @@ import type { ReactElement } from "react"; import { createQueryPreloader } from "../createQueryPreloader"; import { ApolloClient, + ApolloError, ApolloLink, InMemoryCache, NetworkStatus, @@ -14,6 +15,7 @@ import { expectTypeOf } from "expect-type"; import { QueryReference } from "../../cache/QueryReference"; import { DeepPartial, Observable } from "../../../utilities"; import { + Profiler, SimpleCaseData, VariablesCaseData, createProfiler, @@ -25,6 +27,8 @@ import { import { ApolloProvider } from "../../context"; import { RenderOptions, render } from "@testing-library/react"; import { UseReadQueryResult, useReadQuery } from "../../hooks"; +import { GraphQLError } from "graphql"; +import { ErrorBoundary } from "react-error-boundary"; function createDefaultClient(mocks: MockedResponse[]) { return new ApolloClient({ @@ -49,6 +53,31 @@ function renderWithClient( }); } +function createDefaultProfiledComponents< + Snapshot extends { + result: UseReadQueryResult | null; + }, + TData = Snapshot["result"] extends UseReadQueryResult | null ? + TData + : unknown, +>(profiler: Profiler, queryRef: QueryReference) { + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + useTrackRenders(); + profiler.mergeSnapshot({ + result: useReadQuery(queryRef), + } as Partial); + + return null; + } + + return { SuspenseFallback, ReadQueryHook }; +} + test("loads a query and suspends when passed to useReadQuery", async () => { const { query, mocks } = useSimpleCase(); const client = createDefaultClient(mocks); @@ -303,6 +332,67 @@ test("can handle cache updates", async () => { dispose(); }); +test("throws when error is returned", async () => { + // Disable error messages shown by React when an error is thrown to an error + // boundary + using _consoleSpy = spyOnConsole("error"); + const { query } = useSimpleCase(); + const mocks = [ + { request: { query }, result: { errors: [new GraphQLError("Oops")] } }, + ]; + const client = createDefaultClient(mocks); + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + error: null as Error | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query); + + const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents( + Profiler, + queryRef + ); + + function ErrorFallback({ error }: { error: Error }) { + useTrackRenders(); + Profiler.mergeSnapshot({ error }); + + return null; + } + + function App() { + return ( + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(snapshot.error).toEqual( + new ApolloError({ graphQLErrors: [new GraphQLError("Oops")] }) + ); + } + + dispose(); +}); + test("passes context to the link", async () => { interface QueryData { context: Record; From 7542b1fc94ab09ee47ddb2566288a668a1578ab8 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 18:43:43 -0700 Subject: [PATCH 025/133] Add test to ensure errorPolicy: 'all' works as expected --- .../__tests__/createQueryPreloader.test.tsx | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 613581fdba6..362fb9db9ed 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -393,6 +393,70 @@ test("throws when error is returned", async () => { dispose(); }); +test("returns error when error policy is 'all'", async () => { + // Disable error messages shown by React when an error is thrown to an error + // boundary + using _consoleSpy = spyOnConsole("error"); + const { query } = useSimpleCase(); + const mocks = [ + { request: { query }, result: { errors: [new GraphQLError("Oops")] } }, + ]; + const client = createDefaultClient(mocks); + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + error: null as Error | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { errorPolicy: "all" }); + + const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents( + Profiler, + queryRef + ); + + function ErrorFallback({ error }: { error: Error }) { + useTrackRenders(); + Profiler.mergeSnapshot({ error }); + + return null; + } + + function App() { + return ( + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: undefined, + error: new ApolloError({ graphQLErrors: [new GraphQLError("Oops")] }), + networkStatus: NetworkStatus.error, + }); + expect(snapshot.error).toEqual(null); + } + + dispose(); +}); + test("passes context to the link", async () => { interface QueryData { context: Record; From 642247c082d9f8ec647cae4359f55f069bfcb062 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 4 Dec 2023 18:44:44 -0700 Subject: [PATCH 026/133] Add test to ensure errorPolicy: 'ignore' works as expected --- .../__tests__/createQueryPreloader.test.tsx | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 362fb9db9ed..41065be911a 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -457,6 +457,70 @@ test("returns error when error policy is 'all'", async () => { dispose(); }); +test("discards error when error policy is 'none'", async () => { + // Disable error messages shown by React when an error is thrown to an error + // boundary + using _consoleSpy = spyOnConsole("error"); + const { query } = useSimpleCase(); + const mocks = [ + { request: { query }, result: { errors: [new GraphQLError("Oops")] } }, + ]; + const client = createDefaultClient(mocks); + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + error: null as Error | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { errorPolicy: "ignore" }); + + const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents( + Profiler, + queryRef + ); + + function ErrorFallback({ error }: { error: Error }) { + useTrackRenders(); + Profiler.mergeSnapshot({ error }); + + return null; + } + + function App() { + return ( + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: undefined, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(snapshot.error).toEqual(null); + } + + dispose(); +}); + test("passes context to the link", async () => { interface QueryData { context: Record; From 254e0d4bb9d158d53b9363fadfab1f230e48a318 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 14:03:05 -0700 Subject: [PATCH 027/133] Make dispose query ref warning a dev only warning --- src/react/hooks/useReadQuery.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index c61f81e6069..47a4d4d6ff3 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -39,23 +39,25 @@ export interface UseReadQueryResult { export function useReadQuery( queryRef: QueryReference ): UseReadQueryResult { - const didWarnOnDisposedQueryRef = React.useRef(false); const [internalQueryRef, getPromise] = React.useMemo( () => unwrapQueryRef(queryRef), [queryRef] ); - if (!didWarnOnDisposedQueryRef.current && internalQueryRef.disposed) { - invariant.warn( - ` + if (__DEV__) { + const didWarnOnDisposedQueryRef = React.useRef(false); + if (!didWarnOnDisposedQueryRef.current && internalQueryRef.disposed) { + invariant.warn( + ` 'useReadQuery' was called with a disposed queryRef which means the query is no longer watched and cache updates will be missed. This occurs when calling 'dispose' while 'useReadQuery' is still mounted, either by calling 'dispose' too early, or because you are using React's strict mode and calling 'dispose' in a 'useEffect' cleanup function. If you're using a queryRef produced by 'useBackgroundQuery' or 'useLoadableQuery', this is a bug in Apollo Client. Please file an issue. ` - ); - didWarnOnDisposedQueryRef.current = true; + ); + didWarnOnDisposedQueryRef.current = true; + } } const promise = useSyncExternalStore( From fe49b45f33ceef7e63bec5da5211e38c295a75df Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 14:26:39 -0700 Subject: [PATCH 028/133] Fix description of test to match test implementation --- .../query-preloader/__tests__/createQueryPreloader.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 41065be911a..e720dd1c0ac 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -457,7 +457,7 @@ test("returns error when error policy is 'all'", async () => { dispose(); }); -test("discards error when error policy is 'none'", async () => { +test("discards error when error policy is 'ignore'", async () => { // Disable error messages shown by React when an error is thrown to an error // boundary using _consoleSpy = spyOnConsole("error"); From 1238ab4e6fbaa44bf10c449877103408e34e007f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 14:35:56 -0700 Subject: [PATCH 029/133] Add test to ensure queryKey is respected --- .../__tests__/createQueryPreloader.test.tsx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index e720dd1c0ac..da6fc8d416b 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -12,7 +12,7 @@ import { } from "../../../core"; import { MockLink, MockedResponse, wait } from "../../../testing"; import { expectTypeOf } from "expect-type"; -import { QueryReference } from "../../cache/QueryReference"; +import { QueryReference, unwrapQueryRef } from "../../cache/QueryReference"; import { DeepPartial, Observable } from "../../../utilities"; import { Profiler, @@ -592,6 +592,36 @@ test("passes context to the link", async () => { dispose(); }); +test("creates unique query refs when provided with a queryKey", async () => { + const { query } = useSimpleCase(); + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + maxUsageCount: Infinity, + }, + ]; + + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const [queryRef1, dispose1] = preloadQuery(query); + const [queryRef2, dispose2] = preloadQuery(query); + const [queryRef3, dispose3] = preloadQuery(query, { queryKey: 1 }); + + const [unwrappedQueryRef1] = unwrapQueryRef(queryRef1); + const [unwrappedQueryRef2] = unwrapQueryRef(queryRef2); + const [unwrappedQueryRef3] = unwrapQueryRef(queryRef3); + + expect(unwrappedQueryRef2).toBe(unwrappedQueryRef1); + expect(unwrappedQueryRef3).not.toBe(unwrappedQueryRef1); + + dispose1(); + dispose2(); + dispose3(); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From 418c29a83324b3c3155a4dfbdc291ac056746465 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 14:51:09 -0700 Subject: [PATCH 030/133] Add test to ensure returnPartialData works as expected --- .../__tests__/createQueryPreloader.test.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index da6fc8d416b..d49cef37e42 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -622,6 +622,80 @@ test("creates unique query refs when provided with a queryKey", async () => { dispose3(); }); +test("does not suspend and returns partial data when `returnPartialData` is `true`", async () => { + const { query, mocks } = useVariablesCase(); + const partialQuery = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + } + } + `; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + client.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + variables: { id: "1" }, + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { + variables: { id: "1" }, + returnPartialData: true, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents( + Profiler, + queryRef + ); + + function App() { + useTrackRenders(); + return ( + }> + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "1" } }, + networkStatus: NetworkStatus.loading, + error: undefined, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + dispose(); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From 69d363be22a94621df5701265e3460d73775528e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:15:24 -0700 Subject: [PATCH 031/133] Refactor tests to use common setup --- .../__tests__/createQueryPreloader.test.tsx | 188 ++++-------------- 1 file changed, 38 insertions(+), 150 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index d49cef37e42..c820e0132db 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -17,7 +17,6 @@ import { DeepPartial, Observable } from "../../../utilities"; import { Profiler, SimpleCaseData, - VariablesCaseData, createProfiler, spyOnConsole, useSimpleCase, @@ -62,12 +61,12 @@ function createDefaultProfiledComponents< : unknown, >(profiler: Profiler, queryRef: QueryReference) { function SuspenseFallback() { - useTrackRenders(); + useTrackRenders({ name: "SuspenseFallback" }); return

Loading

; } function ReadQueryHook() { - useTrackRenders(); + useTrackRenders({ name: "ReadQueryHook" }); profiler.mergeSnapshot({ result: useReadQuery(queryRef), } as Partial); @@ -78,24 +77,21 @@ function createDefaultProfiledComponents< return { SuspenseFallback, ReadQueryHook }; } -test("loads a query and suspends when passed to useReadQuery", async () => { - const { query, mocks } = useSimpleCase(); - const client = createDefaultClient(mocks); - const preloadQuery = createQueryPreloader(client); +function createDefaultTestApp(queryRef: QueryReference) { const Profiler = createProfiler({ initialSnapshot: { - result: null as UseReadQueryResult | null, + result: null as UseReadQueryResult | null, }, }); - const [queryRef, dispose] = preloadQuery(query); - - function SuspenseFallback() { - useTrackRenders(); - return
Loading
; - } + const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents( + Profiler, + queryRef + ); function App() { + useTrackRenders({ name: "App" }); + return ( }> @@ -103,19 +99,24 @@ test("loads a query and suspends when passed to useReadQuery", async () => { ); } - function ReadQueryHook() { - useTrackRenders(); - Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + return { App, Profiler }; +} - return null; - } +test("loads a query and suspends when passed to useReadQuery", async () => { + const { query, mocks } = useSimpleCase(); + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const [queryRef, dispose] = preloadQuery(query); + + const { App, Profiler } = createDefaultTestApp(queryRef); renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); } { @@ -135,42 +136,19 @@ test("loads a query with variables and suspends when passed to useReadQuery", as const { query, mocks } = useVariablesCase(); const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const Profiler = createProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - }, - }); const [queryRef, dispose] = preloadQuery(query, { variables: { id: "1" }, }); - function SuspenseFallback() { - useTrackRenders(); - return
Loading
; - } - - function App() { - return ( - }> - - - ); - } - - function ReadQueryHook() { - useTrackRenders(); - Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); - - return null; - } + const { App, Profiler } = createDefaultTestApp(queryRef); renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); } { @@ -209,34 +187,10 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { const { query, mocks } = useSimpleCase(); const client = createDefaultClient(mocks); - const Profiler = createProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - }, - }); - const preloadQuery = createQueryPreloader(client); const [queryRef, dispose] = preloadQuery(query); - function SuspenseFallback() { - useTrackRenders(); - return
Loading
; - } - - function App() { - return ( - }> - - - ); - } - - function ReadQueryHook() { - useTrackRenders(); - Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); - - return null; - } + const { App, Profiler } = createDefaultTestApp(queryRef); const { rerender } = renderWithClient(, { client, wrapper: Profiler }); @@ -267,41 +221,18 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { test("can handle cache updates", async () => { const { query, mocks } = useSimpleCase(); const client = createDefaultClient(mocks); - const Profiler = createProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - }, - }); const preloadQuery = createQueryPreloader(client); const [queryRef, dispose] = preloadQuery(query); - function SuspenseFallback() { - useTrackRenders(); - return
Loading
; - } - - function App() { - return ( - }> - - - ); - } - - function ReadQueryHook() { - useTrackRenders(); - Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); - - return null; - } + const { App, Profiler } = createDefaultTestApp(queryRef); renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); } { @@ -364,6 +295,8 @@ test("throws when error is returned", async () => { } function App() { + useTrackRenders(); + return ( }> @@ -378,7 +311,7 @@ test("throws when error is returned", async () => { { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual([App, "SuspenseFallback"]); } { @@ -439,13 +372,13 @@ test("returns error when error policy is 'all'", async () => { { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); } { const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); expect(snapshot.result).toEqual({ data: undefined, error: new ApolloError({ graphQLErrors: [new GraphQLError("Oops")] }), @@ -503,13 +436,13 @@ test("discards error when error policy is 'ignore'", async () => { { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([SuspenseFallback]); + expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); } { const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); expect(snapshot.result).toEqual({ data: undefined, error: undefined, @@ -550,31 +483,7 @@ test("passes context to the link", async () => { context: { valueA: "A", valueB: "B" }, }); - const Profiler = createProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - }, - }); - - function SuspenseFallback() { - useTrackRenders(); - return
Loading
; - } - - function App() { - return ( - }> - - - ); - } - - function ReadQueryHook() { - useTrackRenders(); - Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); - - return null; - } + const { App, Profiler } = createDefaultTestApp(queryRef); renderWithClient(, { client, wrapper: Profiler }); @@ -632,10 +541,7 @@ test("does not suspend and returns partial data when `returnPartialData` is `tru } `; - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new MockLink(mocks), - }); + const client = createDefaultClient(mocks); client.writeQuery({ query: partialQuery, @@ -649,32 +555,14 @@ test("does not suspend and returns partial data when `returnPartialData` is `tru returnPartialData: true, }); - const Profiler = createProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - }, - }); - - const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents( - Profiler, - queryRef - ); - - function App() { - useTrackRenders(); - return ( - }> - - - ); - } + const { App, Profiler } = createDefaultTestApp(queryRef); renderWithClient(, { client, wrapper: Profiler }); { const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(renderedComponents).toStrictEqual(["App", "ReadQueryHook"]); expect(snapshot.result).toEqual({ data: { character: { id: "1" } }, networkStatus: NetworkStatus.loading, @@ -685,7 +573,7 @@ test("does not suspend and returns partial data when `returnPartialData` is `tru { const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); expect(snapshot.result).toEqual({ data: { character: { id: "1", name: "Spider-Man" } }, networkStatus: NetworkStatus.ready, From 342a27d07812a9b1a42dd10f55cf28539e1ed6ff Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:19:22 -0700 Subject: [PATCH 032/133] Add error boundary to default test app --- .../__tests__/createQueryPreloader.test.tsx | 110 ++++-------------- 1 file changed, 20 insertions(+), 90 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index c820e0132db..b4527a67a62 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -81,6 +81,7 @@ function createDefaultTestApp(queryRef: QueryReference) { const Profiler = createProfiler({ initialSnapshot: { result: null as UseReadQueryResult | null, + error: null as Error | null, }, }); @@ -89,13 +90,22 @@ function createDefaultTestApp(queryRef: QueryReference) { queryRef ); + function ErrorFallback({ error }: { error: Error }) { + useTrackRenders({ name: "ErrorFallback" }); + Profiler.mergeSnapshot({ error }); + + return null; + } + function App() { useTrackRenders({ name: "App" }); return ( - }> - - + + }> + + + ); } @@ -272,52 +282,24 @@ test("throws when error is returned", async () => { { request: { query }, result: { errors: [new GraphQLError("Oops")] } }, ]; const client = createDefaultClient(mocks); - const Profiler = createProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - error: null as Error | null, - }, - }); const preloadQuery = createQueryPreloader(client); const [queryRef, dispose] = preloadQuery(query); - const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents( - Profiler, - queryRef - ); - - function ErrorFallback({ error }: { error: Error }) { - useTrackRenders(); - Profiler.mergeSnapshot({ error }); - - return null; - } - - function App() { - useTrackRenders(); - - return ( - - }> - - - - ); - } + const { App, Profiler } = createDefaultTestApp(queryRef); renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([App, "SuspenseFallback"]); + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); } { const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(renderedComponents).toStrictEqual(["ErrorFallback"]); expect(snapshot.error).toEqual( new ApolloError({ graphQLErrors: [new GraphQLError("Oops")] }) ); @@ -335,44 +317,18 @@ test("returns error when error policy is 'all'", async () => { { request: { query }, result: { errors: [new GraphQLError("Oops")] } }, ]; const client = createDefaultClient(mocks); - const Profiler = createProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - error: null as Error | null, - }, - }); const preloadQuery = createQueryPreloader(client); const [queryRef, dispose] = preloadQuery(query, { errorPolicy: "all" }); - const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents( - Profiler, - queryRef - ); - - function ErrorFallback({ error }: { error: Error }) { - useTrackRenders(); - Profiler.mergeSnapshot({ error }); - - return null; - } - - function App() { - return ( - - }> - - - - ); - } + const { App, Profiler } = createDefaultTestApp(queryRef); renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); } { @@ -399,44 +355,18 @@ test("discards error when error policy is 'ignore'", async () => { { request: { query }, result: { errors: [new GraphQLError("Oops")] } }, ]; const client = createDefaultClient(mocks); - const Profiler = createProfiler({ - initialSnapshot: { - result: null as UseReadQueryResult | null, - error: null as Error | null, - }, - }); const preloadQuery = createQueryPreloader(client); const [queryRef, dispose] = preloadQuery(query, { errorPolicy: "ignore" }); - const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents( - Profiler, - queryRef - ); - - function ErrorFallback({ error }: { error: Error }) { - useTrackRenders(); - Profiler.mergeSnapshot({ error }); - - return null; - } - - function App() { - return ( - - }> - - - - ); - } + const { App, Profiler } = createDefaultTestApp(queryRef); renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual(["SuspenseFallback"]); + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); } { From 5fdf1a8d9692fa7dece0fba8afbdea3f29167802 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:21:09 -0700 Subject: [PATCH 033/133] Move everything into createDefaultTestApp --- .../__tests__/createQueryPreloader.test.tsx | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index b4527a67a62..c3c8191a3f4 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -15,7 +15,6 @@ import { expectTypeOf } from "expect-type"; import { QueryReference, unwrapQueryRef } from "../../cache/QueryReference"; import { DeepPartial, Observable } from "../../../utilities"; import { - Profiler, SimpleCaseData, createProfiler, spyOnConsole, @@ -52,31 +51,6 @@ function renderWithClient( }); } -function createDefaultProfiledComponents< - Snapshot extends { - result: UseReadQueryResult | null; - }, - TData = Snapshot["result"] extends UseReadQueryResult | null ? - TData - : unknown, ->(profiler: Profiler, queryRef: QueryReference) { - function SuspenseFallback() { - useTrackRenders({ name: "SuspenseFallback" }); - return

Loading

; - } - - function ReadQueryHook() { - useTrackRenders({ name: "ReadQueryHook" }); - profiler.mergeSnapshot({ - result: useReadQuery(queryRef), - } as Partial); - - return null; - } - - return { SuspenseFallback, ReadQueryHook }; -} - function createDefaultTestApp(queryRef: QueryReference) { const Profiler = createProfiler({ initialSnapshot: { @@ -85,10 +59,17 @@ function createDefaultTestApp(queryRef: QueryReference) { }, }); - const { SuspenseFallback, ReadQueryHook } = createDefaultProfiledComponents( - Profiler, - queryRef - ); + function ReadQueryHook() { + useTrackRenders({ name: "ReadQueryHook" }); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function SuspenseFallback() { + useTrackRenders({ name: "SuspenseFallback" }); + return

Loading

; + } function ErrorFallback({ error }: { error: Error }) { useTrackRenders({ name: "ErrorFallback" }); From 31fb3374206972f4d28128f722cda4b331fa1591 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:28:10 -0700 Subject: [PATCH 034/133] Create new render helper that renders default app --- .../__tests__/createQueryPreloader.test.tsx | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index c3c8191a3f4..b3228625304 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -93,6 +93,24 @@ function createDefaultTestApp(queryRef: QueryReference) { return { App, Profiler }; } +function renderDefaultTestApp({ + client, + queryRef, +}: { + client: ApolloClient; + queryRef: QueryReference; +}) { + const { App, Profiler } = createDefaultTestApp(queryRef); + + const utils = renderWithClient(, { client, wrapper: Profiler }); + + function rerender() { + return utils.rerender(); + } + + return { ...utils, rerender, Profiler }; +} + test("loads a query and suspends when passed to useReadQuery", async () => { const { query, mocks } = useSimpleCase(); const client = createDefaultClient(mocks); @@ -100,9 +118,7 @@ test("loads a query and suspends when passed to useReadQuery", async () => { const [queryRef, dispose] = preloadQuery(query); - const { App, Profiler } = createDefaultTestApp(queryRef); - - renderWithClient(, { client, wrapper: Profiler }); + const { Profiler } = renderDefaultTestApp({ client, queryRef }); { const { renderedComponents } = await Profiler.takeRender(); @@ -132,9 +148,7 @@ test("loads a query with variables and suspends when passed to useReadQuery", as variables: { id: "1" }, }); - const { App, Profiler } = createDefaultTestApp(queryRef); - - renderWithClient(, { client, wrapper: Profiler }); + const { Profiler } = renderDefaultTestApp({ client, queryRef }); { const { renderedComponents } = await Profiler.takeRender(); @@ -181,9 +195,7 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { const preloadQuery = createQueryPreloader(client); const [queryRef, dispose] = preloadQuery(query); - const { App, Profiler } = createDefaultTestApp(queryRef); - - const { rerender } = renderWithClient(, { client, wrapper: Profiler }); + const { Profiler, rerender } = renderDefaultTestApp({ client, queryRef }); await Profiler.takeRender(); await Profiler.takeRender(); @@ -194,7 +206,7 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { await wait(0); - rerender(); + rerender(); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( @@ -203,7 +215,7 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { ) ); - rerender(); + rerender(); // Ensure re-rendering again only shows the warning once expect(console.warn).toHaveBeenCalledTimes(1); @@ -216,9 +228,7 @@ test("can handle cache updates", async () => { const preloadQuery = createQueryPreloader(client); const [queryRef, dispose] = preloadQuery(query); - const { App, Profiler } = createDefaultTestApp(queryRef); - - renderWithClient(, { client, wrapper: Profiler }); + const { Profiler } = renderDefaultTestApp({ client, queryRef }); { const { renderedComponents } = await Profiler.takeRender(); @@ -267,9 +277,7 @@ test("throws when error is returned", async () => { const preloadQuery = createQueryPreloader(client); const [queryRef, dispose] = preloadQuery(query); - const { App, Profiler } = createDefaultTestApp(queryRef); - - renderWithClient(, { client, wrapper: Profiler }); + const { Profiler } = renderDefaultTestApp({ client, queryRef }); { const { renderedComponents } = await Profiler.takeRender(); @@ -302,9 +310,7 @@ test("returns error when error policy is 'all'", async () => { const preloadQuery = createQueryPreloader(client); const [queryRef, dispose] = preloadQuery(query, { errorPolicy: "all" }); - const { App, Profiler } = createDefaultTestApp(queryRef); - - renderWithClient(, { client, wrapper: Profiler }); + const { Profiler } = renderDefaultTestApp({ client, queryRef }); { const { renderedComponents } = await Profiler.takeRender(); @@ -340,9 +346,7 @@ test("discards error when error policy is 'ignore'", async () => { const preloadQuery = createQueryPreloader(client); const [queryRef, dispose] = preloadQuery(query, { errorPolicy: "ignore" }); - const { App, Profiler } = createDefaultTestApp(queryRef); - - renderWithClient(, { client, wrapper: Profiler }); + const { Profiler } = renderDefaultTestApp({ client, queryRef }); { const { renderedComponents } = await Profiler.takeRender(); @@ -394,9 +398,7 @@ test("passes context to the link", async () => { context: { valueA: "A", valueB: "B" }, }); - const { App, Profiler } = createDefaultTestApp(queryRef); - - renderWithClient(, { client, wrapper: Profiler }); + const { Profiler } = renderDefaultTestApp({ client, queryRef }); // initial render await Profiler.takeRender(); @@ -466,9 +468,7 @@ test("does not suspend and returns partial data when `returnPartialData` is `tru returnPartialData: true, }); - const { App, Profiler } = createDefaultTestApp(queryRef); - - renderWithClient(, { client, wrapper: Profiler }); + const { Profiler } = renderDefaultTestApp({ client, queryRef }); { const { snapshot, renderedComponents } = await Profiler.takeRender(); From f746ea63641a53eeda6fde55d055a46c1fd2ab13 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:28:51 -0700 Subject: [PATCH 035/133] Combine renderDefaultTestApp with renderDefaultTestApp --- .../__tests__/createQueryPreloader.test.tsx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index b3228625304..6d9d8853619 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -51,7 +51,13 @@ function renderWithClient( }); } -function createDefaultTestApp(queryRef: QueryReference) { +function renderDefaultTestApp({ + client, + queryRef, +}: { + client: ApolloClient; + queryRef: QueryReference; +}) { const Profiler = createProfiler({ initialSnapshot: { result: null as UseReadQueryResult | null, @@ -90,18 +96,6 @@ function createDefaultTestApp(queryRef: QueryReference) { ); } - return { App, Profiler }; -} - -function renderDefaultTestApp({ - client, - queryRef, -}: { - client: ApolloClient; - queryRef: QueryReference; -}) { - const { App, Profiler } = createDefaultTestApp(queryRef); - const utils = renderWithClient(, { client, wrapper: Profiler }); function rerender() { From 352eee1ff08a6dda78a1a5d840ae205943d1ae03 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:30:51 -0700 Subject: [PATCH 036/133] Combine renderWithClient and renderDefaultTestApp --- .../__tests__/createQueryPreloader.test.tsx | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 6d9d8853619..8d9167e479b 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -1,5 +1,4 @@ import React, { Suspense } from "react"; -import type { ReactElement } from "react"; import { createQueryPreloader } from "../createQueryPreloader"; import { ApolloClient, @@ -23,7 +22,7 @@ import { useVariablesCase, } from "../../../testing/internal"; import { ApolloProvider } from "../../context"; -import { RenderOptions, render } from "@testing-library/react"; +import { render } from "@testing-library/react"; import { UseReadQueryResult, useReadQuery } from "../../hooks"; import { GraphQLError } from "graphql"; import { ErrorBoundary } from "react-error-boundary"; @@ -35,22 +34,6 @@ function createDefaultClient(mocks: MockedResponse[]) { }); } -function renderWithClient( - ui: ReactElement, - { - client, - wrapper: Wrapper = React.Fragment, - }: { client: ApolloClient; wrapper?: RenderOptions["wrapper"] } -) { - return render(ui, { - wrapper: ({ children }) => ( - - {children} - - ), - }); -} - function renderDefaultTestApp({ client, queryRef, @@ -96,7 +79,13 @@ function renderDefaultTestApp({ ); } - const utils = renderWithClient(, { client, wrapper: Profiler }); + const utils = render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); function rerender() { return utils.rerender(); From f96ed3402d9dd34a6cef14c74615fdc657e5b0b6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:40:06 -0700 Subject: [PATCH 037/133] Add test to ensure canonizeResults can be set --- .../__tests__/createQueryPreloader.test.tsx | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 8d9167e479b..974619baa25 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -478,6 +478,69 @@ test("does not suspend and returns partial data when `returnPartialData` is `tru dispose(); }); +test('enables canonical results when canonizeResults is "true"', async () => { + interface Result { + __typename: string; + value: number; + } + + interface QueryData { + results: Result[]; + } + + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); + + const query: TypedDocumentNode = gql` + query { + results { + value + } + } + `; + + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; + + cache.writeQuery({ + query, + data: { results }, + }); + + const client = new ApolloClient({ cache, link: new MockLink([]) }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { canonizeResults: true }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + const { snapshot } = await Profiler.takeRender(); + const resultSet = new Set(snapshot.result?.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(snapshot.result).toEqual({ + data: { results }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); + + dispose(); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From 5d35f837519ee30408c5be661a2533f9f2eeca9e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:45:04 -0700 Subject: [PATCH 038/133] Add test to ensure canonical results can be disabled --- .../__tests__/createQueryPreloader.test.tsx | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 974619baa25..ba8f968bf03 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -541,6 +541,65 @@ test('enables canonical results when canonizeResults is "true"', async () => { dispose(); }); +test("can disable canonical results when the cache's canonizeResults setting is true", async () => { + interface Result { + __typename: string; + value: number; + } + + const cache = new InMemoryCache({ + canonizeResults: true, + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); + + const query: TypedDocumentNode<{ results: Result[] }, never> = gql` + query { + results { + value + } + } + `; + + const results: Result[] = [ + { __typename: "Result", value: 0 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 1 }, + { __typename: "Result", value: 2 }, + { __typename: "Result", value: 3 }, + { __typename: "Result", value: 5 }, + ]; + + cache.writeQuery({ + query, + data: { results }, + }); + + const client = new ApolloClient({ cache, link: new MockLink([]) }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { canonizeResults: false }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + const { snapshot } = await Profiler.takeRender(); + const resultSet = new Set(snapshot.result!.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(snapshot.result).toEqual({ + data: { results }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); + + dispose(); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From a4c577e92f1f50aff1e1ea6742ddd36a8e8c461f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:47:02 -0700 Subject: [PATCH 039/133] Fix formatting in scenarios file --- src/testing/internal/scenarios/index.ts | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts index 6ef12fde436..3044c90da85 100644 --- a/src/testing/internal/scenarios/index.ts +++ b/src/testing/internal/scenarios/index.ts @@ -1,5 +1,6 @@ -import { TypedDocumentNode, gql } from "../../../core/index.js"; -import { MockedResponse } from "../../core/index.js"; +import { gql } from "../../../core/index.js"; +import type { TypedDocumentNode } from "../../../core/index.js"; +import type { MockedResponse } from "../../core/index.js"; export interface SimpleCaseData { greeting: string; @@ -35,17 +36,15 @@ export interface VariablesCaseVariables { } export function useVariablesCase() { - const query: TypedDocumentNode< - VariablesCaseData, - VariablesCaseVariables - > = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name + const query: TypedDocumentNode = + gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + name + } } - } - `; + `; const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"]; const mocks: MockedResponse[] = [...CHARACTERS].map( From a1c2e4c78310dd8c391f094a6a5268395b09ea7f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:53:41 -0700 Subject: [PATCH 040/133] Add test to ensure network-only fetch policy is honored --- .../__tests__/createQueryPreloader.test.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index ba8f968bf03..13e846dfecb 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -247,6 +247,38 @@ test("can handle cache updates", async () => { dispose(); }); +test("ignores cached result and suspends when `fetchPolicy` is network-only", async () => { + const { query, mocks } = useSimpleCase(); + + const client = createDefaultClient(mocks); + client.writeQuery({ query, data: { greeting: "Cached Hello" } }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { + fetchPolicy: "network-only", + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + dispose(); +}); + test("throws when error is returned", async () => { // Disable error messages shown by React when an error is thrown to an error // boundary From 7bad689a4427560a9fcbb791c2888e96b31d9698 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:57:33 -0700 Subject: [PATCH 041/133] Add test for cache-and-network --- .../__tests__/createQueryPreloader.test.tsx | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 13e846dfecb..9705179e29b 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -279,6 +279,77 @@ test("ignores cached result and suspends when `fetchPolicy` is network-only", as dispose(); }); +test("does not cache results when `fetchPolicy` is no-cache", async () => { + const { query, mocks } = useSimpleCase(); + + const client = createDefaultClient(mocks); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { + fetchPolicy: "no-cache", + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(client.extract()).toEqual({}); + + dispose(); +}); + +test("returns initial cache data followed by network data when `fetchPolicy` is cache-and-network", async () => { + const { query, mocks } = useSimpleCase(); + + const client = createDefaultClient(mocks); + client.writeQuery({ query, data: { greeting: "Cached Hello" } }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { + fetchPolicy: "cache-and-network", + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { greeting: "Cached Hello" }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + dispose(); +}); + test("throws when error is returned", async () => { // Disable error messages shown by React when an error is thrown to an error // boundary From 00c36e3dd97cee700f9fc2fcefc9b57aa1232da2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 15:58:41 -0700 Subject: [PATCH 042/133] Add test to ensure cached data is returned immediately --- .../__tests__/createQueryPreloader.test.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 9705179e29b..053dcf3e101 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -350,6 +350,33 @@ test("returns initial cache data followed by network data when `fetchPolicy` is dispose(); }); +test("returns cached data when all data is present in the cache", async () => { + const { query, mocks } = useSimpleCase(); + + const client = createDefaultClient(mocks); + client.writeQuery({ query, data: { greeting: "Cached Hello" } }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { greeting: "Cached Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); + + dispose(); +}); + test("throws when error is returned", async () => { // Disable error messages shown by React when an error is thrown to an error // boundary From 085fd0540d4634fa090b782ef5870e04ff19ff95 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 16:02:55 -0700 Subject: [PATCH 043/133] Add test to ensure partial data in cache is ignored --- .../__tests__/createQueryPreloader.test.tsx | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 053dcf3e101..d00159d4f98 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -377,6 +377,58 @@ test("returns cached data when all data is present in the cache", async () => { dispose(); }); +test("suspends and ignores partial data in the cache", async () => { + const query = gql` + query { + hello + foo + } + `; + + const mocks = [ + { + request: { query }, + result: { data: { hello: "from link", foo: "bar" } }, + delay: 20, + }, + ]; + + const client = createDefaultClient(mocks); + + { + // we expect a "Missing field 'foo' while writing result..." error + // when writing hello to the cache, so we'll silence it + using _consoleSpy = spyOnConsole("error"); + client.writeQuery({ query, data: { hello: "from cache" } }); + } + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { hello: "from link", foo: "bar" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); + + dispose(); +}); + test("throws when error is returned", async () => { // Disable error messages shown by React when an error is thrown to an error // boundary From 5505479a4972e2a07c276544f3109283f9073126 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 16:04:08 -0700 Subject: [PATCH 044/133] Minor tweak to test description --- .../query-preloader/__tests__/createQueryPreloader.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index d00159d4f98..8b0c46e2c22 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -204,7 +204,7 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { expect(console.warn).toHaveBeenCalledTimes(1); }); -test("can handle cache updates", async () => { +test("reacts to cache updates", async () => { const { query, mocks } = useSimpleCase(); const client = createDefaultClient(mocks); From 4674fa05624623a640b7b8a9492eddd3627dc4ac Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 16:12:04 -0700 Subject: [PATCH 045/133] Add test to ensure deferred queries work as expected --- .../__tests__/createQueryPreloader.test.tsx | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 8b0c46e2c22..cb32794d090 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -9,7 +9,12 @@ import { TypedDocumentNode, gql, } from "../../../core"; -import { MockLink, MockedResponse, wait } from "../../../testing"; +import { + MockLink, + MockSubscriptionLink, + MockedResponse, + wait, +} from "../../../testing"; import { expectTypeOf } from "expect-type"; import { QueryReference, unwrapQueryRef } from "../../cache/QueryReference"; import { DeepPartial, Observable } from "../../../utilities"; @@ -782,6 +787,90 @@ test("can disable canonical results when the cache's canonizeResults setting is dispose(); }); +test("suspends deferred queries until initial chunk loads then rerenders with deferred data", async () => { + const query = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["App", "SuspenseFallback"]); + } + + link.simulateResult({ + result: { + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + hasNext: true, + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { greeting: { message: "Hello world", __typename: "Greeting" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }, + true + ); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + dispose(); +}); + describe.skip("type tests", () => { const client = new ApolloClient({ cache: new InMemoryCache(), From 4cdcf96b44c1f84b858de00da1b76319e96bcd46 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 16:50:17 -0700 Subject: [PATCH 046/133] Add first test and implementation for usePreloadedQueryHandlers --- config/jest.config.js | 1 + .../usePreloadedQueryHandlers.test.tsx | 116 ++++++++++++++++++ src/react/hooks/usePreloadedQueryHandlers.ts | 78 ++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx create mode 100644 src/react/hooks/usePreloadedQueryHandlers.ts diff --git a/config/jest.config.js b/config/jest.config.js index de17e13a60f..8a7337e4550 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -36,6 +36,7 @@ const react17TestFileIgnoreList = [ "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", "src/react/hooks/__tests__/useLoadableQuery.test.tsx", + "src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx", "src/react/query-preloader/__tests__/createQueryPreloader.test.tsx", ]; diff --git a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx new file mode 100644 index 00000000000..7ee17041045 --- /dev/null +++ b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx @@ -0,0 +1,116 @@ +import React from "react"; +import { act, render, screen } from "@testing-library/react"; +import { ApolloClient, InMemoryCache, NetworkStatus } from "../../../core"; +import { MockLink } from "../../../testing"; +import { + SimpleCaseData, + createProfiler, + useSimpleCase, + useTrackRenders, +} from "../../../testing/internal"; +import { usePreloadedQueryHandlers } from "../usePreloadedQueryHandlers"; +import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; +import { Suspense } from "react"; +import { createQueryPreloader } from "../../query-preloader/createQueryPreloader"; +import { ApolloProvider } from "../../context"; +import userEvent from "@testing-library/user-event"; + +test("refetches and resuspends when calling refetch", async () => { + const { query, mocks: defaultMocks } = useSimpleCase(); + + const user = userEvent.setup(); + + const mocks = [ + defaultMocks[0], + { + request: { query }, + result: { data: { greeting: "Hello again" } }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { refetch } = usePreloadedQueryHandlers(queryRef); + + return ( + <> + + }> + + + + ); + } + + render(, { + wrapper: ({ children }) => { + return ( + + {children} + + ); + }, + }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + dispose(); +}); diff --git a/src/react/hooks/usePreloadedQueryHandlers.ts b/src/react/hooks/usePreloadedQueryHandlers.ts new file mode 100644 index 00000000000..1f25d8a6c5f --- /dev/null +++ b/src/react/hooks/usePreloadedQueryHandlers.ts @@ -0,0 +1,78 @@ +import * as React from "rehackt"; +import { + unwrapQueryRef, + updateWrappedQueryRef, +} from "../cache/QueryReference.js"; +import type { QueryReference } from "../cache/QueryReference.js"; +import type { OperationVariables } from "../../core/types.js"; +import type { RefetchFunction, FetchMoreFunction } from "./useSuspenseQuery.js"; +import type { + FetchMoreQueryOptions, + WatchQueryOptions, +} from "../../core/watchQueryOptions.js"; + +type UpdateOptionsFunction = ( + options: WatchQueryOptions +) => void; + +export interface UsePreloadedQueryHandlersResult< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> { + refetch: RefetchFunction; + fetchMore: FetchMoreFunction; + updateOptions: UpdateOptionsFunction; +} + +export function usePreloadedQueryHandlers< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + queryRef: QueryReference +): UsePreloadedQueryHandlersResult { + const [, forceUpdate] = React.useState(0); + + const refetch: RefetchFunction = React.useCallback( + (variables) => { + const [internalQueryRef] = unwrapQueryRef(queryRef); + const promise = internalQueryRef.refetch(variables); + + updateWrappedQueryRef(queryRef, internalQueryRef.promise); + forceUpdate((c) => c + 1); + + return promise; + }, + [queryRef] + ); + + const fetchMore: FetchMoreFunction = React.useCallback( + (options) => { + const [internalQueryRef] = unwrapQueryRef(queryRef); + const promise = internalQueryRef.fetchMore( + options as FetchMoreQueryOptions + ); + + updateWrappedQueryRef(queryRef, internalQueryRef.promise); + forceUpdate((c) => c + 1); + + return promise; + }, + [queryRef] + ); + + const updateOptions: UpdateOptionsFunction = + React.useCallback( + (options) => { + const [internalQueryRef] = unwrapQueryRef(queryRef); + + if (internalQueryRef.didChangeOptions(options)) { + const promise = internalQueryRef.applyOptions(options); + updateWrappedQueryRef(queryRef, promise); + forceUpdate((c) => c + 1); + } + }, + [queryRef] + ); + + return { refetch, fetchMore, updateOptions }; +} From aaf4b856763402d0b4a701c3376bc62a0311258c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 17:36:25 -0700 Subject: [PATCH 047/133] Add transition support for usePreloadedQueryHandlers --- src/react/cache/QueryReference.ts | 6 +- .../usePreloadedQueryHandlers.test.tsx | 136 +++++++++++++++++- src/react/hooks/usePreloadedQueryHandlers.ts | 31 ++-- 3 files changed, 154 insertions(+), 19 deletions(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index ee097944677..5289aa315a1 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -54,6 +54,10 @@ export function wrapQueryRef( }; } +export function getWrappedPromise(queryRef: QueryReference) { + return queryRef[PROMISE_SYMBOL]; +} + export function unwrapQueryRef( queryRef: QueryReference ): [InternalQueryReference, () => QueryRefPromise] { @@ -67,7 +71,7 @@ export function unwrapQueryRef( // it instead of the older promise which may contain outdated data. internalQueryRef.promise.status === "fulfilled" ? internalQueryRef.promise - : queryRef[PROMISE_SYMBOL], + : getWrappedPromise(queryRef), ]; } diff --git a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx index 7ee17041045..f52a370e067 100644 --- a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx +++ b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx @@ -1,7 +1,13 @@ import React from "react"; -import { act, render, screen } from "@testing-library/react"; -import { ApolloClient, InMemoryCache, NetworkStatus } from "../../../core"; -import { MockLink } from "../../../testing"; +import { act, render, screen, waitFor } from "@testing-library/react"; +import { + ApolloClient, + InMemoryCache, + NetworkStatus, + TypedDocumentNode, + gql, +} from "../../../core"; +import { MockLink, MockedResponse } from "../../../testing"; import { SimpleCaseData, createProfiler, @@ -114,3 +120,127 @@ test("refetches and resuspends when calling refetch", async () => { dispose(); }); + +test("`refetch` works with startTransition", async () => { + type Variables = { + id: string; + }; + + interface Data { + todo: { + id: string; + name: string; + completed: boolean; + }; + } + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { variables: { id: "1" } }); + + function App() { + const { refetch } = usePreloadedQueryHandlers(queryRef); + const [isPending, startTransition] = React.useTransition(); + + return ( + <> + + }> + + + + ); + } + + function SuspenseFallback() { + return

Loading

; + } + + function Todo() { + const { data } = useReadQuery(queryRef); + const { todo } = data; + + return ( +
+ {todo.name} + {todo.completed && " (completed)"} +
+ ); + } + + render(); + + expect(screen.getByText("Loading")).toBeInTheDocument(); + + const todo = await screen.findByTestId("todo"); + + expect(todo).toBeInTheDocument(); + expect(todo).toHaveTextContent("Clean room"); + + const button = screen.getByText("Refetch"); + await act(() => user.click(button)); + + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + + // We can ensure this works with isPending from useTransition in the process + expect(button).toBeDisabled(); + + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); + + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Clean room (completed)"); + }); + + dispose(); +}); diff --git a/src/react/hooks/usePreloadedQueryHandlers.ts b/src/react/hooks/usePreloadedQueryHandlers.ts index 1f25d8a6c5f..a0034cb82d3 100644 --- a/src/react/hooks/usePreloadedQueryHandlers.ts +++ b/src/react/hooks/usePreloadedQueryHandlers.ts @@ -1,7 +1,9 @@ import * as React from "rehackt"; import { + getWrappedPromise, unwrapQueryRef, updateWrappedQueryRef, + wrapQueryRef, } from "../cache/QueryReference.js"; import type { QueryReference } from "../cache/QueryReference.js"; import type { OperationVariables } from "../../core/types.js"; @@ -30,48 +32,47 @@ export function usePreloadedQueryHandlers< >( queryRef: QueryReference ): UsePreloadedQueryHandlersResult { - const [, forceUpdate] = React.useState(0); + const [wrappedQueryRef, setWrappedQueryRef] = React.useState(queryRef); + const [internalQueryRef] = unwrapQueryRef(queryRef); + + // To ensure we can support React transitions, this hook needs to manage the + // queryRef state and apply React's state value immediately to the existing + // queryRef since this hook doesn't return the queryRef directly + updateWrappedQueryRef(queryRef, getWrappedPromise(wrappedQueryRef)); const refetch: RefetchFunction = React.useCallback( (variables) => { - const [internalQueryRef] = unwrapQueryRef(queryRef); const promise = internalQueryRef.refetch(variables); - updateWrappedQueryRef(queryRef, internalQueryRef.promise); - forceUpdate((c) => c + 1); + setWrappedQueryRef(wrapQueryRef(internalQueryRef)); return promise; }, - [queryRef] + [internalQueryRef] ); const fetchMore: FetchMoreFunction = React.useCallback( (options) => { - const [internalQueryRef] = unwrapQueryRef(queryRef); const promise = internalQueryRef.fetchMore( options as FetchMoreQueryOptions ); - updateWrappedQueryRef(queryRef, internalQueryRef.promise); - forceUpdate((c) => c + 1); + setWrappedQueryRef(wrapQueryRef(internalQueryRef)); return promise; }, - [queryRef] + [internalQueryRef] ); const updateOptions: UpdateOptionsFunction = React.useCallback( (options) => { - const [internalQueryRef] = unwrapQueryRef(queryRef); - if (internalQueryRef.didChangeOptions(options)) { - const promise = internalQueryRef.applyOptions(options); - updateWrappedQueryRef(queryRef, promise); - forceUpdate((c) => c + 1); + internalQueryRef.applyOptions(options); + setWrappedQueryRef(wrapQueryRef(internalQueryRef)); } }, - [queryRef] + [internalQueryRef] ); return { refetch, fetchMore, updateOptions }; From afc56e79259c9759b51547038947252dcff8569e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 17:40:54 -0700 Subject: [PATCH 048/133] Add test to ensure usePreloadedQueryHandlers does not overwrite promise --- .../usePreloadedQueryHandlers.test.tsx | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx index f52a370e067..8f5e5cef107 100644 --- a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx +++ b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx @@ -121,6 +121,104 @@ test("refetches and resuspends when calling refetch", async () => { dispose(); }); +test("does not interfere with updates to the query from useReadQuery", async () => { + const { query, mocks } = useSimpleCase(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + // We can ignore the return result here since we are testing the mechanics + // of this hook to ensure it doesn't interfere with the updates from + // useReadQuery + usePreloadedQueryHandlers(queryRef); + + return ( + }> + + + ); + } + + const { rerender } = render(, { + wrapper: ({ children }) => { + return ( + + {children} + + ); + }, + }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ query, data: { greeting: "Hello again" } }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + rerender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + dispose(); +}); + test("`refetch` works with startTransition", async () => { type Variables = { id: string; From 3b297238d48199637453cd99952a3be32ca07d9c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 17:58:54 -0700 Subject: [PATCH 049/133] Remove updateOptions function for now --- src/react/hooks/usePreloadedQueryHandlers.ts | 23 ++------------------ 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/src/react/hooks/usePreloadedQueryHandlers.ts b/src/react/hooks/usePreloadedQueryHandlers.ts index a0034cb82d3..1b900ce988b 100644 --- a/src/react/hooks/usePreloadedQueryHandlers.ts +++ b/src/react/hooks/usePreloadedQueryHandlers.ts @@ -8,14 +8,7 @@ import { import type { QueryReference } from "../cache/QueryReference.js"; import type { OperationVariables } from "../../core/types.js"; import type { RefetchFunction, FetchMoreFunction } from "./useSuspenseQuery.js"; -import type { - FetchMoreQueryOptions, - WatchQueryOptions, -} from "../../core/watchQueryOptions.js"; - -type UpdateOptionsFunction = ( - options: WatchQueryOptions -) => void; +import type { FetchMoreQueryOptions } from "../../core/watchQueryOptions.js"; export interface UsePreloadedQueryHandlersResult< TData = unknown, @@ -23,7 +16,6 @@ export interface UsePreloadedQueryHandlersResult< > { refetch: RefetchFunction; fetchMore: FetchMoreFunction; - updateOptions: UpdateOptionsFunction; } export function usePreloadedQueryHandlers< @@ -64,16 +56,5 @@ export function usePreloadedQueryHandlers< [internalQueryRef] ); - const updateOptions: UpdateOptionsFunction = - React.useCallback( - (options) => { - if (internalQueryRef.didChangeOptions(options)) { - internalQueryRef.applyOptions(options); - setWrappedQueryRef(wrapQueryRef(internalQueryRef)); - } - }, - [internalQueryRef] - ); - - return { refetch, fetchMore, updateOptions }; + return { refetch, fetchMore }; } From 583d47abe470d5bea1bc07c0494ebf8a4a9f1667 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 18:02:24 -0700 Subject: [PATCH 050/133] Minor adjustment to test description --- src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx index 8f5e5cef107..a0e7afa2de1 100644 --- a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx +++ b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx @@ -121,7 +121,7 @@ test("refetches and resuspends when calling refetch", async () => { dispose(); }); -test("does not interfere with updates to the query from useReadQuery", async () => { +test("does not interfere with updates from useReadQuery", async () => { const { query, mocks } = useSimpleCase(); const client = new ApolloClient({ From ec3c6152140bed700f0954a29ae4ed3f5f1fe367 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 18:16:41 -0700 Subject: [PATCH 051/133] Add TVariables to QueryReference --- src/react/cache/QueryReference.ts | 14 +++++-- .../__tests__/useBackgroundQuery.test.tsx | 40 ++++++++++--------- src/react/hooks/useBackgroundQuery.ts | 24 ++++++----- src/react/hooks/useLoadableQuery.ts | 2 +- .../query-preloader/createQueryPreloader.ts | 2 +- 5 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 5289aa315a1..2a6ce4b5365 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -35,8 +35,11 @@ const PROMISE_SYMBOL: unique symbol = Symbol(); * A child component reading the `QueryReference` via {@link useReadQuery} will * suspend until the promise resolves. */ -export interface QueryReference { - readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; +export interface QueryReference< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> { + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; [PROMISE_SYMBOL]: QueryRefPromise; } @@ -96,7 +99,10 @@ type ObservedOptions = Pick< (typeof OBSERVED_CHANGED_OPTIONS)[number] >; -export class InternalQueryReference { +export class InternalQueryReference< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> { public result: ApolloQueryResult; public readonly key: QueryKey = {}; public readonly observable: ObservableQuery; @@ -242,7 +248,7 @@ export class InternalQueryReference { }; } - refetch(variables: OperationVariables | undefined) { + refetch(variables: Partial | undefined) { return this.initiateFetch(this.observable.refetch(variables)); } diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 85b857e47a5..1d57c7112b6 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -5661,7 +5661,7 @@ describe("useBackgroundQuery", () => { }); expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); expectTypeOf(inferredQueryRef).not.toEqualTypeOf< QueryReference @@ -5673,10 +5673,10 @@ describe("useBackgroundQuery", () => { >(query, { skip: true }); expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); // TypeScript is too smart and using a `const` or `let` boolean variable @@ -5691,10 +5691,10 @@ describe("useBackgroundQuery", () => { }); expectTypeOf(dynamicQueryRef).toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); expectTypeOf(dynamicQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); }); @@ -5705,7 +5705,7 @@ describe("useBackgroundQuery", () => { expectTypeOf(inferredQueryRef).toEqualTypeOf(); expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); const [explicitQueryRef] = useBackgroundQuery< @@ -5715,7 +5715,7 @@ describe("useBackgroundQuery", () => { expectTypeOf(explicitQueryRef).toEqualTypeOf(); expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); }); @@ -5731,10 +5731,10 @@ describe("useBackgroundQuery", () => { ); expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); const [explicitQueryRef] = useBackgroundQuery< @@ -5743,10 +5743,10 @@ describe("useBackgroundQuery", () => { >(query, options.skip ? skipToken : undefined); expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference | undefined + QueryReference | undefined >(); expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); }); @@ -5762,22 +5762,24 @@ describe("useBackgroundQuery", () => { ); expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference> | undefined + | QueryReference, VariablesCaseVariables> + | undefined >(); expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); - const [explicitQueryRef] = useBackgroundQuery( - query, - options.skip ? skipToken : { returnPartialData: true } - ); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, options.skip ? skipToken : { returnPartialData: true }); expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference> | undefined + | QueryReference, VariablesCaseVariables> + | undefined >(); expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference + QueryReference >(); }); }); diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 47484ad45b8..4424c6329b7 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -51,7 +51,8 @@ export function useBackgroundQuery< DeepPartial | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial - : TData + : TData, + TVariables > | (TOptions["skip"] extends boolean ? undefined : never) ), @@ -68,7 +69,7 @@ export function useBackgroundQuery< errorPolicy: "ignore" | "all"; } ): [ - QueryReference | undefined>, + QueryReference | undefined, TVariables>, UseBackgroundQueryResult, ]; @@ -81,7 +82,7 @@ export function useBackgroundQuery< errorPolicy: "ignore" | "all"; } ): [ - QueryReference, + QueryReference, UseBackgroundQueryResult, ]; @@ -95,7 +96,7 @@ export function useBackgroundQuery< returnPartialData: true; } ): [ - QueryReference> | undefined, + QueryReference, TVariables> | undefined, UseBackgroundQueryResult, ]; @@ -108,7 +109,7 @@ export function useBackgroundQuery< returnPartialData: true; } ): [ - QueryReference>, + QueryReference, TVariables>, UseBackgroundQueryResult, ]; @@ -121,7 +122,7 @@ export function useBackgroundQuery< skip: boolean; } ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ]; @@ -131,7 +132,10 @@ export function useBackgroundQuery< >( query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer -): [QueryReference, UseBackgroundQueryResult]; +): [ + QueryReference, + UseBackgroundQueryResult, +]; export function useBackgroundQuery< TData = unknown, @@ -152,7 +156,7 @@ export function useBackgroundQuery< returnPartialData: true; }) ): [ - QueryReference> | undefined, + QueryReference, TVariables> | undefined, UseBackgroundQueryResult, ]; @@ -163,7 +167,7 @@ export function useBackgroundQuery< query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ]; @@ -177,7 +181,7 @@ export function useBackgroundQuery< Partial>) | BackgroundQueryHookOptionsNoInfer = Object.create(null) ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ] { const client = useApolloClient(options.client); diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 282988d8a16..2161fc480ab 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -43,7 +43,7 @@ export type UseLoadableQueryResult< TVariables extends OperationVariables = OperationVariables, > = [ LoadQueryFunction, - QueryReference | null, + QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 0861e43e0dd..7b9e87cea10 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -45,7 +45,7 @@ export type PreloadQueryOptions< export type PreloadedQueryResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, -> = [QueryReference, dispose: () => void]; +> = [QueryReference, dispose: () => void]; type PreloadQueryOptionsArg< TVariables extends OperationVariables, From a1ef63ece03d83a4213327176cd3b290c698489d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 18:19:28 -0700 Subject: [PATCH 052/133] Infer TVariables from QueryReference for usePreloadedQueryHandlers --- src/react/hooks/usePreloadedQueryHandlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/hooks/usePreloadedQueryHandlers.ts b/src/react/hooks/usePreloadedQueryHandlers.ts index 1b900ce988b..062c60f2cd6 100644 --- a/src/react/hooks/usePreloadedQueryHandlers.ts +++ b/src/react/hooks/usePreloadedQueryHandlers.ts @@ -22,7 +22,7 @@ export function usePreloadedQueryHandlers< TData = unknown, TVariables extends OperationVariables = OperationVariables, >( - queryRef: QueryReference + queryRef: QueryReference ): UsePreloadedQueryHandlersResult { const [wrappedQueryRef, setWrappedQueryRef] = React.useState(queryRef); const [internalQueryRef] = unwrapQueryRef(queryRef); From 9cdbc30308b0dd3e2474b2713566399c106a471f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 18:27:31 -0700 Subject: [PATCH 053/133] Add tests to ensure refetchWritePolicy works correctly --- .../usePreloadedQueryHandlers.test.tsx | 401 ++++++++++++++++++ 1 file changed, 401 insertions(+) diff --git a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx index a0e7afa2de1..a0917929452 100644 --- a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx +++ b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx @@ -219,6 +219,407 @@ test("does not interfere with updates from useReadQuery", async () => { dispose(); }); +test('honors refetchWritePolicy set to "merge"', async () => { + const query: TypedDocumentNode< + { primes: number[] }, + { min: number; max: number } + > = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + interface QueryData { + primes: number[]; + } + + const mocks = [ + { + request: { query, variables: { min: 0, max: 12 } }, + result: { data: { primes: [2, 3, 5, 7, 11] } }, + delay: 10, + }, + { + request: { query, variables: { min: 12, max: 30 } }, + result: { data: { primes: [13, 17, 19, 23, 29] } }, + delay: 10, + }, + ]; + + const mergeParams: [number[] | undefined, number[]][] = []; + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + primes: { + keyArgs: false, + merge(existing: number[] | undefined, incoming: number[]) { + mergeParams.push([existing, incoming]); + return existing ? existing.concat(incoming) : incoming; + }, + }, + }, + }, + }, + }); + + const user = userEvent.setup(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const preloadQuery = createQueryPreloader(client); + const [queryRef, dispose] = preloadQuery(query, { + refetchWritePolicy: "merge", + variables: { min: 0, max: 12 }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

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

Loading

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

Loading

; + } + + function ReadQueryHook() { + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { refetch } = usePreloadedQueryHandlers(queryRef); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + render(, { + wrapper: ({ children }) => { + return ( + + {children} + + ); + }, + }); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [2, 3, 5, 7, 11] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { primes: [13, 17, 19, 23, 29] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [undefined, [13, 17, 19, 23, 29]], + ]); + } + + await expect(Profiler).not.toRerender(); + + dispose(); +}); + test("`refetch` works with startTransition", async () => { type Variables = { id: string; From 447194c53bcb36ec37544e866e0559a5cfae3003 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 18:33:08 -0700 Subject: [PATCH 054/133] Publicly export usePreloadedQueryHandlers --- src/react/hooks/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 8a725261f40..345ea719931 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -16,6 +16,8 @@ export type { UseLoadableQueryResult, } from "./useLoadableQuery.js"; export { useLoadableQuery } from "./useLoadableQuery.js"; +export type { UsePreloadedQueryHandlersResult } from "./usePreloadedQueryHandlers.js"; +export { usePreloadedQueryHandlers } from "./usePreloadedQueryHandlers.js"; export type { UseReadQueryResult } from "./useReadQuery.js"; export { useReadQuery } from "./useReadQuery.js"; export { skipToken } from "./constants.js"; From 90e280fa740b14e350a57615f3916c4a11047aae Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 18:33:47 -0700 Subject: [PATCH 055/133] Update snapshot exports tests --- src/__tests__/__snapshots__/exports.ts.snap | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 70229c88a17..73e911c0d35 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -20,6 +20,7 @@ Array [ "checkFetcher", "concat", "createHttpLink", + "createQueryPreloader", "createSignalIfSupported", "defaultDataIdFromObject", "defaultPrinter", @@ -61,6 +62,7 @@ Array [ "useLazyQuery", "useLoadableQuery", "useMutation", + "usePreloadedQueryHandlers", "useQuery", "useReactiveVar", "useReadQuery", @@ -265,6 +267,7 @@ Array [ "ApolloConsumer", "ApolloProvider", "DocumentType", + "createQueryPreloader", "getApolloContext", "operationName", "parser", @@ -276,6 +279,7 @@ Array [ "useLazyQuery", "useLoadableQuery", "useMutation", + "usePreloadedQueryHandlers", "useQuery", "useReactiveVar", "useReadQuery", @@ -320,6 +324,7 @@ Array [ "useLazyQuery", "useLoadableQuery", "useMutation", + "usePreloadedQueryHandlers", "useQuery", "useReactiveVar", "useReadQuery", From 2270972e22da29f8e5040684ec0ab5af223b5a27 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Wed, 6 Dec 2023 18:43:41 -0700 Subject: [PATCH 056/133] WIP changeset --- .changeset/rare-snakes-melt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rare-snakes-melt.md diff --git a/.changeset/rare-snakes-melt.md b/.changeset/rare-snakes-melt.md new file mode 100644 index 00000000000..23b1d7c4838 --- /dev/null +++ b/.changeset/rare-snakes-melt.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Add a new `createQueryPreloader` function that allows you to preload queries outside of React From 59f25e0267a4c1984463ee319adfbb5658e4314f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 00:07:37 -0700 Subject: [PATCH 057/133] Include a retain function on the wrapped query ref --- src/react/cache/QueryReference.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 2a6ce4b5365..7bcc11a4232 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -23,6 +23,8 @@ type QueryRefPromise = PromiseWithState>; type Listener = (promise: QueryRefPromise) => void; +type DisposeFn = () => void; + type FetchMoreOptions = Parameters< ObservableQuery["fetchMore"] >[0]; @@ -41,6 +43,7 @@ export interface QueryReference< > { readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; [PROMISE_SYMBOL]: QueryRefPromise; + retain: () => DisposeFn; } interface InternalQueryReferenceOptions { @@ -52,6 +55,7 @@ export function wrapQueryRef( internalQueryRef: InternalQueryReference ): QueryReference { return { + retain: () => internalQueryRef.retain(), [QUERY_REFERENCE_SYMBOL]: internalQueryRef, [PROMISE_SYMBOL]: internalQueryRef.promise, }; From 489dddf4b0f16a055716bcdb7a0daa5bedac6d2e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 00:20:19 -0700 Subject: [PATCH 058/133] Don't auto retain query ref when preloading --- .../__tests__/createQueryPreloader.test.tsx | 122 +++++++----------- .../query-preloader/createQueryPreloader.ts | 21 +-- 2 files changed, 51 insertions(+), 92 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index cb32794d090..f825b6015cd 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -104,7 +104,7 @@ test("loads a query and suspends when passed to useReadQuery", async () => { const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query); + const queryRef = preloadQuery(query); const { Profiler } = renderDefaultTestApp({ client, queryRef }); @@ -123,8 +123,6 @@ test("loads a query and suspends when passed to useReadQuery", async () => { networkStatus: NetworkStatus.ready, }); } - - dispose(); }); test("loads a query with variables and suspends when passed to useReadQuery", async () => { @@ -132,7 +130,7 @@ test("loads a query with variables and suspends when passed to useReadQuery", as const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { + const queryRef = preloadQuery(query, { variables: { id: "1" }, }); @@ -153,8 +151,6 @@ test("loads a query with variables and suspends when passed to useReadQuery", as networkStatus: NetworkStatus.ready, }); } - - dispose(); }); test("tears down the query when calling dispose", async () => { @@ -162,7 +158,8 @@ test("tears down the query when calling dispose", async () => { const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const [, dispose] = preloadQuery(query); + const queryRef = preloadQuery(query); + const dispose = queryRef.retain(); expect(client.getObservableQueries().size).toBe(1); expect(client).toHaveSuspenseCacheEntryUsing(query); @@ -181,7 +178,8 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query); + const queryRef = preloadQuery(query); + const dispose = queryRef.retain(); const { Profiler, rerender } = renderDefaultTestApp({ client, queryRef }); @@ -214,7 +212,7 @@ test("reacts to cache updates", async () => { const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query); + const queryRef = preloadQuery(query); const { Profiler } = renderDefaultTestApp({ client, queryRef }); @@ -248,8 +246,6 @@ test("reacts to cache updates", async () => { networkStatus: NetworkStatus.ready, }); } - - dispose(); }); test("ignores cached result and suspends when `fetchPolicy` is network-only", async () => { @@ -259,7 +255,7 @@ test("ignores cached result and suspends when `fetchPolicy` is network-only", as client.writeQuery({ query, data: { greeting: "Cached Hello" } }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { + const queryRef = preloadQuery(query, { fetchPolicy: "network-only", }); @@ -280,8 +276,6 @@ test("ignores cached result and suspends when `fetchPolicy` is network-only", as networkStatus: NetworkStatus.ready, }); } - - dispose(); }); test("does not cache results when `fetchPolicy` is no-cache", async () => { @@ -290,7 +284,7 @@ test("does not cache results when `fetchPolicy` is no-cache", async () => { const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { + const queryRef = preloadQuery(query, { fetchPolicy: "no-cache", }); @@ -313,8 +307,6 @@ test("does not cache results when `fetchPolicy` is no-cache", async () => { } expect(client.extract()).toEqual({}); - - dispose(); }); test("returns initial cache data followed by network data when `fetchPolicy` is cache-and-network", async () => { @@ -324,7 +316,7 @@ test("returns initial cache data followed by network data when `fetchPolicy` is client.writeQuery({ query, data: { greeting: "Cached Hello" } }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { + const queryRef = preloadQuery(query, { fetchPolicy: "cache-and-network", }); @@ -351,8 +343,6 @@ test("returns initial cache data followed by network data when `fetchPolicy` is networkStatus: NetworkStatus.ready, }); } - - dispose(); }); test("returns cached data when all data is present in the cache", async () => { @@ -362,7 +352,7 @@ test("returns cached data when all data is present in the cache", async () => { client.writeQuery({ query, data: { greeting: "Cached Hello" } }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query); + const queryRef = preloadQuery(query); const { Profiler } = renderDefaultTestApp({ client, queryRef }); @@ -378,8 +368,6 @@ test("returns cached data when all data is present in the cache", async () => { } await expect(Profiler).not.toRerender(); - - dispose(); }); test("suspends and ignores partial data in the cache", async () => { @@ -408,7 +396,7 @@ test("suspends and ignores partial data in the cache", async () => { } const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query); + const queryRef = preloadQuery(query); const { Profiler } = renderDefaultTestApp({ client, queryRef }); @@ -430,8 +418,6 @@ test("suspends and ignores partial data in the cache", async () => { } await expect(Profiler).not.toRerender(); - - dispose(); }); test("throws when error is returned", async () => { @@ -445,7 +431,7 @@ test("throws when error is returned", async () => { const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query); + const queryRef = preloadQuery(query); const { Profiler } = renderDefaultTestApp({ client, queryRef }); @@ -463,8 +449,6 @@ test("throws when error is returned", async () => { new ApolloError({ graphQLErrors: [new GraphQLError("Oops")] }) ); } - - dispose(); }); test("returns error when error policy is 'all'", async () => { @@ -478,7 +462,7 @@ test("returns error when error policy is 'all'", async () => { const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { errorPolicy: "all" }); + const queryRef = preloadQuery(query, { errorPolicy: "all" }); const { Profiler } = renderDefaultTestApp({ client, queryRef }); @@ -499,8 +483,6 @@ test("returns error when error policy is 'all'", async () => { }); expect(snapshot.error).toEqual(null); } - - dispose(); }); test("discards error when error policy is 'ignore'", async () => { @@ -514,7 +496,7 @@ test("discards error when error policy is 'ignore'", async () => { const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { errorPolicy: "ignore" }); + const queryRef = preloadQuery(query, { errorPolicy: "ignore" }); const { Profiler } = renderDefaultTestApp({ client, queryRef }); @@ -535,8 +517,6 @@ test("discards error when error policy is 'ignore'", async () => { }); expect(snapshot.error).toEqual(null); } - - dispose(); }); test("passes context to the link", async () => { @@ -564,7 +544,7 @@ test("passes context to the link", async () => { }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { + const queryRef = preloadQuery(query, { context: { valueA: "A", valueB: "B" }, }); @@ -580,8 +560,6 @@ test("passes context to the link", async () => { networkStatus: NetworkStatus.ready, error: undefined, }); - - dispose(); }); test("creates unique query refs when provided with a queryKey", async () => { @@ -598,9 +576,9 @@ test("creates unique query refs when provided with a queryKey", async () => { const client = createDefaultClient(mocks); const preloadQuery = createQueryPreloader(client); - const [queryRef1, dispose1] = preloadQuery(query); - const [queryRef2, dispose2] = preloadQuery(query); - const [queryRef3, dispose3] = preloadQuery(query, { queryKey: 1 }); + const queryRef1 = preloadQuery(query); + const queryRef2 = preloadQuery(query); + const queryRef3 = preloadQuery(query, { queryKey: 1 }); const [unwrappedQueryRef1] = unwrapQueryRef(queryRef1); const [unwrappedQueryRef2] = unwrapQueryRef(queryRef2); @@ -608,10 +586,6 @@ test("creates unique query refs when provided with a queryKey", async () => { expect(unwrappedQueryRef2).toBe(unwrappedQueryRef1); expect(unwrappedQueryRef3).not.toBe(unwrappedQueryRef1); - - dispose1(); - dispose2(); - dispose3(); }); test("does not suspend and returns partial data when `returnPartialData` is `true`", async () => { @@ -633,7 +607,7 @@ test("does not suspend and returns partial data when `returnPartialData` is `tru }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { + const queryRef = preloadQuery(query, { variables: { id: "1" }, returnPartialData: true, }); @@ -661,8 +635,6 @@ test("does not suspend and returns partial data when `returnPartialData` is `tru error: undefined, }); } - - dispose(); }); test('enables canonical results when canonizeResults is "true"', async () => { @@ -708,7 +680,7 @@ test('enables canonical results when canonizeResults is "true"', async () => { const client = new ApolloClient({ cache, link: new MockLink([]) }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { canonizeResults: true }); + const queryRef = preloadQuery(query, { canonizeResults: true }); const { Profiler } = renderDefaultTestApp({ client, queryRef }); @@ -724,8 +696,6 @@ test('enables canonical results when canonizeResults is "true"', async () => { expect(resultSet.size).toBe(5); expect(values).toEqual([0, 1, 2, 3, 5]); - - dispose(); }); test("can disable canonical results when the cache's canonizeResults setting is true", async () => { @@ -768,7 +738,7 @@ test("can disable canonical results when the cache's canonizeResults setting is const client = new ApolloClient({ cache, link: new MockLink([]) }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { canonizeResults: false }); + const queryRef = preloadQuery(query, { canonizeResults: false }); const { Profiler } = renderDefaultTestApp({ client, queryRef }); @@ -783,8 +753,6 @@ test("can disable canonical results when the cache's canonizeResults setting is }); expect(resultSet.size).toBe(6); expect(values).toEqual([0, 1, 1, 2, 3, 5]); - - dispose(); }); test("suspends deferred queries until initial chunk loads then rerenders with deferred data", async () => { @@ -805,7 +773,7 @@ test("suspends deferred queries until initial chunk loads then rerenders with de const client = new ApolloClient({ cache: new InMemoryCache(), link }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query); + const queryRef = preloadQuery(query); const { Profiler } = renderDefaultTestApp({ client, queryRef }); @@ -867,8 +835,6 @@ test("suspends deferred queries until initial chunk loads then rerenders with de networkStatus: NetworkStatus.ready, }); } - - dispose(); }); describe.skip("type tests", () => { @@ -1004,7 +970,7 @@ describe.skip("type tests", () => { test("returns QueryReference when TData cannot be inferred", () => { const query = gql``; - const [queryRef] = preloadQuery(query); + const queryRef = preloadQuery(query); expectTypeOf(queryRef).toEqualTypeOf>(); }); @@ -1012,14 +978,14 @@ describe.skip("type tests", () => { test("returns QueryReference in default case", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery(query); + const queryRef = preloadQuery(query); expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery(query); + const queryRef = preloadQuery(query); expectTypeOf(queryRef).toEqualTypeOf>(); } @@ -1028,7 +994,7 @@ describe.skip("type tests", () => { test("returns QueryReference with errorPolicy: 'ignore'", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery(query, { errorPolicy: "ignore" }); + const queryRef = preloadQuery(query, { errorPolicy: "ignore" }); expectTypeOf(queryRef).toEqualTypeOf< QueryReference @@ -1037,7 +1003,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { errorPolicy: "ignore", }); @@ -1050,7 +1016,7 @@ describe.skip("type tests", () => { test("returns QueryReference with errorPolicy: 'all'", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery(query, { errorPolicy: "all" }); + const queryRef = preloadQuery(query, { errorPolicy: "all" }); expectTypeOf(queryRef).toEqualTypeOf< QueryReference @@ -1059,7 +1025,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { errorPolicy: "all", }); @@ -1072,14 +1038,14 @@ describe.skip("type tests", () => { test("returns QueryReference with errorPolicy: 'none'", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery(query, { errorPolicy: "none" }); + const queryRef = preloadQuery(query, { errorPolicy: "none" }); expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { errorPolicy: "none", }); @@ -1090,7 +1056,7 @@ describe.skip("type tests", () => { test("returns QueryReference> with returnPartialData: true", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery(query, { returnPartialData: true }); + const queryRef = preloadQuery(query, { returnPartialData: true }); expectTypeOf(queryRef).toEqualTypeOf< QueryReference> @@ -1099,7 +1065,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { returnPartialData: true, }); @@ -1112,14 +1078,14 @@ describe.skip("type tests", () => { test("returns QueryReference> with returnPartialData: false", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery(query, { returnPartialData: false }); + const queryRef = preloadQuery(query, { returnPartialData: false }); expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { returnPartialData: false, }); @@ -1130,14 +1096,14 @@ describe.skip("type tests", () => { test("returns QueryReference when passing an option unrelated to TData", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery(query, { canonizeResults: true }); + const queryRef = preloadQuery(query, { canonizeResults: true }); expectTypeOf(queryRef).toEqualTypeOf>(); } { const query = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { canonizeResults: true, }); @@ -1148,7 +1114,7 @@ describe.skip("type tests", () => { test("handles combinations of options", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { returnPartialData: true, errorPolicy: "ignore", }); @@ -1160,7 +1126,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { returnPartialData: true, errorPolicy: "ignore", }); @@ -1172,7 +1138,7 @@ describe.skip("type tests", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { returnPartialData: true, errorPolicy: "none", }); @@ -1184,7 +1150,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { returnPartialData: true, errorPolicy: "none", }); @@ -1198,7 +1164,7 @@ describe.skip("type tests", () => { test("returns correct TData type when combined with options unrelated to TData", () => { { const query: TypedDocumentNode = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { canonizeResults: true, returnPartialData: true, errorPolicy: "none", @@ -1211,7 +1177,7 @@ describe.skip("type tests", () => { { const query = gql``; - const [queryRef] = preloadQuery(query, { + const queryRef = preloadQuery(query, { canonizeResults: true, returnPartialData: true, errorPolicy: "none", diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 7b9e87cea10..0007da0859b 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -42,11 +42,6 @@ export type PreloadQueryOptions< refetchWritePolicy?: RefetchWritePolicy; } & VariablesOption; -export type PreloadedQueryResult< - TData = unknown, - TVariables extends OperationVariables = OperationVariables, -> = [QueryReference, dispose: () => void]; - type PreloadQueryOptionsArg< TVariables extends OperationVariables, TOptions = unknown, @@ -66,7 +61,7 @@ export function createQueryPreloader(client: ApolloClient) { >( query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg, TOptions> - ): PreloadedQueryResult< + ): QueryReference< TOptions["errorPolicy"] extends "ignore" | "all" ? TOptions["returnPartialData"] extends true ? DeepPartial | undefined @@ -85,7 +80,7 @@ export function createQueryPreloader(client: ApolloClient) { returnPartialData: true; errorPolicy: "ignore" | "all"; } - ): PreloadedQueryResult | undefined, TVariables>; + ): QueryReference | undefined, TVariables>; function preloadQuery< TData = unknown, @@ -95,7 +90,7 @@ export function createQueryPreloader(client: ApolloClient) { options: PreloadQueryOptions> & { errorPolicy: "ignore" | "all"; } - ): PreloadedQueryResult; + ): QueryReference; function preloadQuery< TData = unknown, @@ -105,7 +100,7 @@ export function createQueryPreloader(client: ApolloClient) { options: PreloadQueryOptions & { returnPartialData: true; } - ): PreloadedQueryResult, TVariables>; + ): QueryReference, TVariables>; function preloadQuery< TData = unknown, @@ -113,7 +108,7 @@ export function createQueryPreloader(client: ApolloClient) { >( query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg> - ): PreloadedQueryResult; + ): QueryReference; function preloadQuery< TData = unknown, @@ -123,7 +118,7 @@ export function createQueryPreloader(client: ApolloClient) { options: PreloadQueryOptions & VariablesOption = Object.create( null ) - ): PreloadedQueryResult { + ): QueryReference { const { variables, queryKey = [], ...watchQueryOptions } = options; const cacheKey: CacheKey = [ @@ -140,9 +135,7 @@ export function createQueryPreloader(client: ApolloClient) { } as WatchQueryOptions) ); - const dispose = queryRef.retain(); - - return [wrapQueryRef(queryRef), dispose]; + return wrapQueryRef(queryRef); } return preloadQuery; From ebbea5f7aec56692ae5885c900c9db25db4e0768 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 00:20:26 -0700 Subject: [PATCH 059/133] Add todo tests for retaining --- .../__tests__/createQueryPreloader.test.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index f825b6015cd..770e0a95752 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -172,6 +172,16 @@ test("tears down the query when calling dispose", async () => { expect(client).not.toHaveSuspenseCacheEntryUsing(query); }); +test.todo( + "Auto disposes of the query ref if not retained within the given time" +); +test.todo( + "useReadQuery auto-retains the queryRef and disposes of it when unmounted" +); +test.todo( + "queryRef is not disposed when useReadQuery unmounts when manually retained" +); + test("useReadQuery warns when called with a disposed queryRef", async () => { using _consoleSpy = spyOnConsole("warn"); const { query, mocks } = useSimpleCase(); From 13c4e9adf2fa839bc18c89ecd95a6c4b73311aae Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 00:22:58 -0700 Subject: [PATCH 060/133] Remove never variable types in type tests --- .../__tests__/createQueryPreloader.test.tsx | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 770e0a95752..26732a5010f 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -987,7 +987,7 @@ describe.skip("type tests", () => { test("returns QueryReference in default case", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const queryRef = preloadQuery(query); expectTypeOf(queryRef).toEqualTypeOf>(); @@ -1003,7 +1003,7 @@ describe.skip("type tests", () => { test("returns QueryReference with errorPolicy: 'ignore'", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const queryRef = preloadQuery(query, { errorPolicy: "ignore" }); expectTypeOf(queryRef).toEqualTypeOf< @@ -1025,7 +1025,7 @@ describe.skip("type tests", () => { test("returns QueryReference with errorPolicy: 'all'", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const queryRef = preloadQuery(query, { errorPolicy: "all" }); expectTypeOf(queryRef).toEqualTypeOf< @@ -1047,7 +1047,7 @@ describe.skip("type tests", () => { test("returns QueryReference with errorPolicy: 'none'", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const queryRef = preloadQuery(query, { errorPolicy: "none" }); expectTypeOf(queryRef).toEqualTypeOf>(); @@ -1065,7 +1065,7 @@ describe.skip("type tests", () => { test("returns QueryReference> with returnPartialData: true", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const queryRef = preloadQuery(query, { returnPartialData: true }); expectTypeOf(queryRef).toEqualTypeOf< @@ -1087,7 +1087,7 @@ describe.skip("type tests", () => { test("returns QueryReference> with returnPartialData: false", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const queryRef = preloadQuery(query, { returnPartialData: false }); expectTypeOf(queryRef).toEqualTypeOf>(); @@ -1105,7 +1105,7 @@ describe.skip("type tests", () => { test("returns QueryReference when passing an option unrelated to TData", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const queryRef = preloadQuery(query, { canonizeResults: true }); expectTypeOf(queryRef).toEqualTypeOf>(); @@ -1123,7 +1123,7 @@ describe.skip("type tests", () => { test("handles combinations of options", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const queryRef = preloadQuery(query, { returnPartialData: true, errorPolicy: "ignore", @@ -1147,7 +1147,7 @@ describe.skip("type tests", () => { } { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const queryRef = preloadQuery(query, { returnPartialData: true, errorPolicy: "none", @@ -1173,7 +1173,7 @@ describe.skip("type tests", () => { test("returns correct TData type when combined with options unrelated to TData", () => { { - const query: TypedDocumentNode = gql``; + const query: TypedDocumentNode = gql``; const queryRef = preloadQuery(query, { canonizeResults: true, returnPartialData: true, From f108a2d68669dc3eb54b48ab167d8dd36b56a2c1 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 00:25:00 -0700 Subject: [PATCH 061/133] Update usePreloadedQueryHandlers tests to use new signature --- .../usePreloadedQueryHandlers.test.tsx | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx index a0917929452..fa0f7d23d25 100644 --- a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx +++ b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx @@ -47,7 +47,7 @@ test("refetches and resuspends when calling refetch", async () => { }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query); + const queryRef = preloadQuery(query); function SuspenseFallback() { useTrackRenders(); @@ -117,8 +117,6 @@ test("refetches and resuspends when calling refetch", async () => { networkStatus: NetworkStatus.ready, }); } - - dispose(); }); test("does not interfere with updates from useReadQuery", async () => { @@ -136,7 +134,7 @@ test("does not interfere with updates from useReadQuery", async () => { }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query); + const queryRef = preloadQuery(query); function SuspenseFallback() { useTrackRenders(); @@ -215,8 +213,6 @@ test("does not interfere with updates from useReadQuery", async () => { networkStatus: NetworkStatus.ready, }); } - - dispose(); }); test('honors refetchWritePolicy set to "merge"', async () => { @@ -276,7 +272,7 @@ test('honors refetchWritePolicy set to "merge"', async () => { }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { + const queryRef = preloadQuery(query, { refetchWritePolicy: "merge", variables: { min: 0, max: 12 }, }); @@ -351,8 +347,6 @@ test('honors refetchWritePolicy set to "merge"', async () => { } await expect(Profiler).not.toRerender(); - - dispose(); }); test('honors refetchWritePolicy set to "overwrite"', async () => { @@ -412,7 +406,7 @@ test('honors refetchWritePolicy set to "overwrite"', async () => { }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { + const queryRef = preloadQuery(query, { refetchWritePolicy: "overwrite", variables: { min: 0, max: 12 }, }); @@ -484,8 +478,6 @@ test('honors refetchWritePolicy set to "overwrite"', async () => { } await expect(Profiler).not.toRerender(); - - dispose(); }); test('defaults refetchWritePolicy to "overwrite"', async () => { @@ -545,7 +537,7 @@ test('defaults refetchWritePolicy to "overwrite"', async () => { }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { + const queryRef = preloadQuery(query, { variables: { min: 0, max: 12 }, }); @@ -616,8 +608,6 @@ test('defaults refetchWritePolicy to "overwrite"', async () => { } await expect(Profiler).not.toRerender(); - - dispose(); }); test("`refetch` works with startTransition", async () => { @@ -667,7 +657,7 @@ test("`refetch` works with startTransition", async () => { }); const preloadQuery = createQueryPreloader(client); - const [queryRef, dispose] = preloadQuery(query, { variables: { id: "1" } }); + const queryRef = preloadQuery(query, { variables: { id: "1" } }); function App() { const { refetch } = usePreloadedQueryHandlers(queryRef); @@ -740,6 +730,4 @@ test("`refetch` works with startTransition", async () => { await waitFor(() => { expect(todo).toHaveTextContent("Clean room (completed)"); }); - - dispose(); }); From 3122df4e805276b03712dc4ce5825832e28821bc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 00:26:08 -0700 Subject: [PATCH 062/133] Auto retain queryRef in useReadQuery --- src/react/hooks/useReadQuery.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 47a4d4d6ff3..a7aa43ae458 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -44,6 +44,8 @@ export function useReadQuery( [queryRef] ); + React.useEffect(() => internalQueryRef.retain(), [internalQueryRef]); + if (__DEV__) { const didWarnOnDisposedQueryRef = React.useRef(false); if (!didWarnOnDisposedQueryRef.current && internalQueryRef.disposed) { From cf10e84a05184c4c823b7d2ca26ca5627bf74b9e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 00:28:25 -0700 Subject: [PATCH 063/133] Remove deleted export from query preloader --- src/react/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/react/index.ts b/src/react/index.ts index 03f78e9cdad..4205851640d 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -16,7 +16,6 @@ export { DocumentType, operationName, parser } from "./parser/index.js"; export type { PreloadQueryOptions, PreloadQueryFetchPolicy, - PreloadedQueryResult, } from "./query-preloader/createQueryPreloader.js"; export { createQueryPreloader } from "./query-preloader/createQueryPreloader.js"; From ef13893cc48bdbd39f1dab70317adae4b9a79e62 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 11:47:13 -0700 Subject: [PATCH 064/133] Remove extra newlines in warning --- src/react/hooks/useReadQuery.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index a7aa43ae458..664289e0220 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -49,15 +49,11 @@ export function useReadQuery( if (__DEV__) { const didWarnOnDisposedQueryRef = React.useRef(false); if (!didWarnOnDisposedQueryRef.current && internalQueryRef.disposed) { - invariant.warn( - ` -'useReadQuery' was called with a disposed queryRef which means the query is no longer watched and cache updates will be missed. + invariant.warn(`'useReadQuery' was called with a disposed queryRef which means the query is no longer watched and cache updates will be missed. This occurs when calling 'dispose' while 'useReadQuery' is still mounted, either by calling 'dispose' too early, or because you are using React's strict mode and calling 'dispose' in a 'useEffect' cleanup function. -If you're using a queryRef produced by 'useBackgroundQuery' or 'useLoadableQuery', this is a bug in Apollo Client. Please file an issue. -` - ); +If you're using a queryRef produced by 'useBackgroundQuery' or 'useLoadableQuery', this is a bug in Apollo Client. Please file an issue.`); didWarnOnDisposedQueryRef.current = true; } } From 88ceeff620c077ab0ec3d836368b24c2dcf26c11 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 13:25:09 -0700 Subject: [PATCH 065/133] Fix more references to TVariables for QueryReference --- src/react/cache/QueryReference.ts | 36 +++++++++++++++++++------------ 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 7bcc11a4232..f6b58759a47 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -2,6 +2,7 @@ import { equal } from "@wry/equality"; import type { ApolloError, ApolloQueryResult, + FetchMoreQueryOptions, ObservableQuery, OperationVariables, WatchQueryOptions, @@ -25,9 +26,10 @@ type Listener = (promise: QueryRefPromise) => void; type DisposeFn = () => void; -type FetchMoreOptions = Parameters< - ObservableQuery["fetchMore"] ->[0]; +type FetchMoreOptions< + TData, + TVariables extends OperationVariables, +> = Parameters["fetchMore"]>[0]; const QUERY_REFERENCE_SYMBOL: unique symbol = Symbol(); const PROMISE_SYMBOL: unique symbol = Symbol(); @@ -51,9 +53,9 @@ interface InternalQueryReferenceOptions { autoDisposeTimeoutMs?: number; } -export function wrapQueryRef( - internalQueryRef: InternalQueryReference -): QueryReference { +export function wrapQueryRef( + internalQueryRef: InternalQueryReference +): QueryReference { return { retain: () => internalQueryRef.retain(), [QUERY_REFERENCE_SYMBOL]: internalQueryRef, @@ -61,13 +63,15 @@ export function wrapQueryRef( }; } -export function getWrappedPromise(queryRef: QueryReference) { +export function getWrappedPromise( + queryRef: QueryReference +) { return queryRef[PROMISE_SYMBOL]; } -export function unwrapQueryRef( - queryRef: QueryReference -): [InternalQueryReference, () => QueryRefPromise] { +export function unwrapQueryRef( + queryRef: QueryReference +): [InternalQueryReference, () => QueryRefPromise] { const internalQueryRef = queryRef[QUERY_REFERENCE_SYMBOL]; return [ @@ -109,7 +113,7 @@ export class InternalQueryReference< > { public result: ApolloQueryResult; public readonly key: QueryKey = {}; - public readonly observable: ObservableQuery; + public readonly observable: ObservableQuery; public promise: QueryRefPromise; @@ -125,7 +129,7 @@ export class InternalQueryReference< private references = 0; constructor( - observable: ObservableQuery, + observable: ObservableQuery, options: InternalQueryReferenceOptions ) { this.handleNext = this.handleNext.bind(this); @@ -256,8 +260,12 @@ export class InternalQueryReference< return this.initiateFetch(this.observable.refetch(variables)); } - fetchMore(options: FetchMoreOptions) { - return this.initiateFetch(this.observable.fetchMore(options)); + fetchMore(options: FetchMoreOptions) { + return this.initiateFetch( + this.observable.fetchMore( + options as FetchMoreQueryOptions + ) + ); } private dispose() { From 0a80d4fac98a66bc7151e46def6aa23a36b4bb0b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 13:25:33 -0700 Subject: [PATCH 066/133] Add a toPromise function with some tests --- src/react/cache/QueryReference.ts | 23 +++++++ .../cache/__tests__/QueryReference.test.ts | 61 +++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 src/react/cache/__tests__/QueryReference.test.ts diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index f6b58759a47..911ca725c6b 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -46,6 +46,13 @@ export interface QueryReference< readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; [PROMISE_SYMBOL]: QueryRefPromise; retain: () => DisposeFn; + toPromise(): Promise>; + toPromise(options: { + maxWait: number; + }): Promise | undefined>; + toPromise(options: { + maxWait?: number | undefined; + }): Promise>; } interface InternalQueryReferenceOptions { @@ -57,6 +64,22 @@ export function wrapQueryRef( internalQueryRef: InternalQueryReference ): QueryReference { return { + // @ts-expect-error Target signature provides too few arguments. Expected 1 or more, but got 0. https://github.com/microsoft/TypeScript/issues/54539 + toPromise(options) { + const promise = + internalQueryRef.promise.status === "fulfilled" ? + internalQueryRef.promise + : this[PROMISE_SYMBOL]; + + if (options?.maxWait) { + return Promise.race([ + promise, + new Promise((resolve) => setTimeout(resolve, options.maxWait)), + ]); + } + + return promise; + }, retain: () => internalQueryRef.retain(), [QUERY_REFERENCE_SYMBOL]: internalQueryRef, [PROMISE_SYMBOL]: internalQueryRef.promise, diff --git a/src/react/cache/__tests__/QueryReference.test.ts b/src/react/cache/__tests__/QueryReference.test.ts new file mode 100644 index 00000000000..46bcb83af58 --- /dev/null +++ b/src/react/cache/__tests__/QueryReference.test.ts @@ -0,0 +1,61 @@ +import { expectTypeOf } from "expect-type"; +import type { QueryReference } from "../QueryReference"; +import { InternalQueryReference, wrapQueryRef } from "../QueryReference"; +import { ApolloClient, ApolloQueryResult, InMemoryCache } from "../../../core"; +import { useSimpleCase } from "../../../testing/internal"; +import { MockLink } from "../../../testing"; + +// Used for type tests +declare const queryRef: QueryReference<{ foo: string }>; + +test("toPromise returns promise that resolves with the data", async () => { + const { query, mocks } = useSimpleCase(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + const observable = client.watchQuery({ query }); + const internalQueryReference = new InternalQueryReference(observable, {}); + const queryRef = wrapQueryRef(internalQueryReference); + + const result = await queryRef.toPromise(); + + expect(result.data).toEqual({ greeting: "Hello" }); +}); + +test("toPromise returns undefined if maxWait is met", async () => { + const { query } = useSimpleCase(); + const mocks = [ + { request: { query }, result: { data: { greeting: "Hello" } }, delay: 100 }, + ]; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + const observable = client.watchQuery({ query }); + const internalQueryReference = new InternalQueryReference(observable, {}); + const queryRef = wrapQueryRef(internalQueryReference); + + const result = await queryRef.toPromise({ maxWait: 10 }); + + expect(result).toBeUndefined(); +}); + +describe.skip("type tests", () => { + test("toPromise returns correct type depending on presence of maxWait", () => { + expectTypeOf(queryRef.toPromise()).toMatchTypeOf< + Promise> + >(); + expectTypeOf(queryRef.toPromise({})).toMatchTypeOf< + Promise> + >(); + expectTypeOf(queryRef.toPromise({ maxWait: undefined })).toMatchTypeOf< + Promise> + >(); + expectTypeOf(queryRef.toPromise({ maxWait: 1000 })).toMatchTypeOf< + Promise | undefined> + >(); + }); +}); From 3ae9a525a8996a74a2ea97518c516d3bf46ff1f3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 14:02:23 -0700 Subject: [PATCH 067/133] Fix ts issues from change to TVariables --- src/react/cache/QueryReference.ts | 6 ++---- src/react/cache/SuspenseCache.ts | 11 +++++++---- src/react/hooks/useLoadableQuery.ts | 7 ++++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 911ca725c6b..ab0d48a899e 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -86,9 +86,7 @@ export function wrapQueryRef( }; } -export function getWrappedPromise( - queryRef: QueryReference -) { +export function getWrappedPromise(queryRef: QueryReference) { return queryRef[PROMISE_SYMBOL]; } @@ -110,7 +108,7 @@ export function unwrapQueryRef( } export function updateWrappedQueryRef( - queryRef: QueryReference, + queryRef: QueryReference, promise: QueryRefPromise ) { queryRef[PROMISE_SYMBOL] = promise; diff --git a/src/react/cache/SuspenseCache.ts b/src/react/cache/SuspenseCache.ts index 36641c76cb4..efa5cc97b56 100644 --- a/src/react/cache/SuspenseCache.ts +++ b/src/react/cache/SuspenseCache.ts @@ -1,5 +1,5 @@ import { Trie } from "@wry/trie"; -import type { ObservableQuery } from "../../core/index.js"; +import type { ObservableQuery, OperationVariables } from "../../core/index.js"; import { canUseWeakMap } from "../../utilities/index.js"; import { InternalQueryReference } from "./QueryReference.js"; import type { CacheKey } from "./types.js"; @@ -28,12 +28,15 @@ export class SuspenseCache { this.options = options; } - getQueryRef( + getQueryRef< + TData = any, + TVariables extends OperationVariables = OperationVariables, + >( cacheKey: CacheKey, - createObservable: () => ObservableQuery + createObservable: () => ObservableQuery ) { const ref = this.queryRefs.lookupArray(cacheKey) as { - current?: InternalQueryReference; + current?: InternalQueryReference; }; if (!ref.current) { diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 2161fc480ab..80111307ecd 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -119,9 +119,10 @@ export function useLoadableQuery< const watchQueryOptions = useWatchQueryOptions({ client, query, options }); const { queryKey = [] } = options; - const [queryRef, setQueryRef] = React.useState | null>( - null - ); + const [queryRef, setQueryRef] = React.useState | null>(null); const internalQueryRef = queryRef && unwrapQueryRef(queryRef)[0]; From 5a2de9fa5b220a36d334de4b3ed1d2e551574ad6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Thu, 7 Dec 2023 15:22:03 -0700 Subject: [PATCH 068/133] Default to any for QueryReference TVariables --- src/react/cache/QueryReference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index ab0d48a899e..a06a023c260 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -41,7 +41,7 @@ const PROMISE_SYMBOL: unique symbol = Symbol(); */ export interface QueryReference< TData = unknown, - TVariables extends OperationVariables = OperationVariables, + TVariables extends OperationVariables = any, > { readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; [PROMISE_SYMBOL]: QueryRefPromise; From d05316dfdfd47346b4940061e67ec771ee333861 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 16:49:25 -0700 Subject: [PATCH 069/133] Simplify types to get it working --- src/react/cache/QueryReference.ts | 22 ++++++++-------------- src/react/cache/SuspenseCache.ts | 13 +++++-------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index a06a023c260..3e18cc19ac0 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -39,11 +39,8 @@ const PROMISE_SYMBOL: unique symbol = Symbol(); * A child component reading the `QueryReference` via {@link useReadQuery} will * suspend until the promise resolves. */ -export interface QueryReference< - TData = unknown, - TVariables extends OperationVariables = any, -> { - readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; +export interface QueryReference { + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; [PROMISE_SYMBOL]: QueryRefPromise; retain: () => DisposeFn; toPromise(): Promise>; @@ -92,7 +89,7 @@ export function getWrappedPromise(queryRef: QueryReference) { export function unwrapQueryRef( queryRef: QueryReference -): [InternalQueryReference, () => QueryRefPromise] { +): [InternalQueryReference, () => QueryRefPromise] { const internalQueryRef = queryRef[QUERY_REFERENCE_SYMBOL]; return [ @@ -128,13 +125,10 @@ type ObservedOptions = Pick< (typeof OBSERVED_CHANGED_OPTIONS)[number] >; -export class InternalQueryReference< - TData = unknown, - TVariables extends OperationVariables = OperationVariables, -> { +export class InternalQueryReference { public result: ApolloQueryResult; public readonly key: QueryKey = {}; - public readonly observable: ObservableQuery; + public readonly observable: ObservableQuery; public promise: QueryRefPromise; @@ -150,7 +144,7 @@ export class InternalQueryReference< private references = 0; constructor( - observable: ObservableQuery, + observable: ObservableQuery, options: InternalQueryReferenceOptions ) { this.handleNext = this.handleNext.bind(this); @@ -277,11 +271,11 @@ export class InternalQueryReference< }; } - refetch(variables: Partial | undefined) { + refetch(variables: OperationVariables | undefined) { return this.initiateFetch(this.observable.refetch(variables)); } - fetchMore(options: FetchMoreOptions) { + fetchMore(options: FetchMoreOptions) { return this.initiateFetch( this.observable.fetchMore( options as FetchMoreQueryOptions diff --git a/src/react/cache/SuspenseCache.ts b/src/react/cache/SuspenseCache.ts index efa5cc97b56..d835b8732cc 100644 --- a/src/react/cache/SuspenseCache.ts +++ b/src/react/cache/SuspenseCache.ts @@ -1,5 +1,5 @@ import { Trie } from "@wry/trie"; -import type { ObservableQuery, OperationVariables } from "../../core/index.js"; +import type { ObservableQuery } from "../../core/index.js"; import { canUseWeakMap } from "../../utilities/index.js"; import { InternalQueryReference } from "./QueryReference.js"; import type { CacheKey } from "./types.js"; @@ -28,15 +28,12 @@ export class SuspenseCache { this.options = options; } - getQueryRef< - TData = any, - TVariables extends OperationVariables = OperationVariables, - >( + getQueryRef( cacheKey: CacheKey, - createObservable: () => ObservableQuery + createObservable: () => ObservableQuery ) { const ref = this.queryRefs.lookupArray(cacheKey) as { - current?: InternalQueryReference; + current?: InternalQueryReference; }; if (!ref.current) { @@ -48,6 +45,6 @@ export class SuspenseCache { }); } - return ref.current; + return ref.current as InternalQueryReference; } } From feb37e20a6d7b0e7ef9a7812fab6b0173a855973 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:01:04 -0700 Subject: [PATCH 070/133] Remove query reference test for now --- .../cache/__tests__/QueryReference.test.ts | 61 ------------------- 1 file changed, 61 deletions(-) delete mode 100644 src/react/cache/__tests__/QueryReference.test.ts diff --git a/src/react/cache/__tests__/QueryReference.test.ts b/src/react/cache/__tests__/QueryReference.test.ts deleted file mode 100644 index 46bcb83af58..00000000000 --- a/src/react/cache/__tests__/QueryReference.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { expectTypeOf } from "expect-type"; -import type { QueryReference } from "../QueryReference"; -import { InternalQueryReference, wrapQueryRef } from "../QueryReference"; -import { ApolloClient, ApolloQueryResult, InMemoryCache } from "../../../core"; -import { useSimpleCase } from "../../../testing/internal"; -import { MockLink } from "../../../testing"; - -// Used for type tests -declare const queryRef: QueryReference<{ foo: string }>; - -test("toPromise returns promise that resolves with the data", async () => { - const { query, mocks } = useSimpleCase(); - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new MockLink(mocks), - }); - const observable = client.watchQuery({ query }); - const internalQueryReference = new InternalQueryReference(observable, {}); - const queryRef = wrapQueryRef(internalQueryReference); - - const result = await queryRef.toPromise(); - - expect(result.data).toEqual({ greeting: "Hello" }); -}); - -test("toPromise returns undefined if maxWait is met", async () => { - const { query } = useSimpleCase(); - const mocks = [ - { request: { query }, result: { data: { greeting: "Hello" } }, delay: 100 }, - ]; - - const client = new ApolloClient({ - cache: new InMemoryCache(), - link: new MockLink(mocks), - }); - const observable = client.watchQuery({ query }); - const internalQueryReference = new InternalQueryReference(observable, {}); - const queryRef = wrapQueryRef(internalQueryReference); - - const result = await queryRef.toPromise({ maxWait: 10 }); - - expect(result).toBeUndefined(); -}); - -describe.skip("type tests", () => { - test("toPromise returns correct type depending on presence of maxWait", () => { - expectTypeOf(queryRef.toPromise()).toMatchTypeOf< - Promise> - >(); - expectTypeOf(queryRef.toPromise({})).toMatchTypeOf< - Promise> - >(); - expectTypeOf(queryRef.toPromise({ maxWait: undefined })).toMatchTypeOf< - Promise> - >(); - expectTypeOf(queryRef.toPromise({ maxWait: 1000 })).toMatchTypeOf< - Promise | undefined> - >(); - }); -}); From 0cce22ffe350e77755e04f810a9452e944a226fd Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:01:16 -0700 Subject: [PATCH 071/133] Only support toPromise without maxTime --- src/react/cache/QueryReference.ts | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 3e18cc19ac0..09ce135a9f2 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -44,12 +44,6 @@ export interface QueryReference { [PROMISE_SYMBOL]: QueryRefPromise; retain: () => DisposeFn; toPromise(): Promise>; - toPromise(options: { - maxWait: number; - }): Promise | undefined>; - toPromise(options: { - maxWait?: number | undefined; - }): Promise>; } interface InternalQueryReferenceOptions { @@ -58,24 +52,13 @@ interface InternalQueryReferenceOptions { } export function wrapQueryRef( - internalQueryRef: InternalQueryReference + internalQueryRef: InternalQueryReference ): QueryReference { return { - // @ts-expect-error Target signature provides too few arguments. Expected 1 or more, but got 0. https://github.com/microsoft/TypeScript/issues/54539 - toPromise(options) { - const promise = - internalQueryRef.promise.status === "fulfilled" ? + toPromise() { + return internalQueryRef.promise.status === "fulfilled" ? internalQueryRef.promise : this[PROMISE_SYMBOL]; - - if (options?.maxWait) { - return Promise.race([ - promise, - new Promise((resolve) => setTimeout(resolve, options.maxWait)), - ]); - } - - return promise; }, retain: () => internalQueryRef.retain(), [QUERY_REFERENCE_SYMBOL]: internalQueryRef, From ec282287f5b59ca042e5dc09bcea1a3a7673ca9c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:05:26 -0700 Subject: [PATCH 072/133] Remove unneeded arg to getWrappedPromise --- src/react/cache/QueryReference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 09ce135a9f2..db1f01cc59c 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -66,7 +66,7 @@ export function wrapQueryRef( }; } -export function getWrappedPromise(queryRef: QueryReference) { +export function getWrappedPromise(queryRef: QueryReference) { return queryRef[PROMISE_SYMBOL]; } From 844551db46bdee1f8d31ee8f6affc6ca5ebcc6bc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:14:22 -0700 Subject: [PATCH 073/133] move promise unwrapping to toPromise function on the query ref --- src/react/cache/QueryReference.ts | 20 +++++------------ .../__tests__/useBackgroundQuery.test.tsx | 22 +++++++++---------- src/react/hooks/useBackgroundQuery.ts | 2 +- src/react/hooks/useLoadableQuery.ts | 2 +- src/react/hooks/usePreloadedQueryHandlers.ts | 2 +- src/react/hooks/useReadQuery.ts | 6 ++--- 6 files changed, 23 insertions(+), 31 deletions(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index db1f01cc59c..b25e92aacd8 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -43,7 +43,7 @@ export interface QueryReference { readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; [PROMISE_SYMBOL]: QueryRefPromise; retain: () => DisposeFn; - toPromise(): Promise>; + toPromise(): QueryRefPromise; } interface InternalQueryReferenceOptions { @@ -56,6 +56,9 @@ export function wrapQueryRef( ): QueryReference { return { toPromise() { + // There is a chance the query ref's promise has been updated in the time + // the original promise had been suspended. In that case, we want to use + // it instead of the older promise which may contain outdated data. return internalQueryRef.promise.status === "fulfilled" ? internalQueryRef.promise : this[PROMISE_SYMBOL]; @@ -72,19 +75,8 @@ export function getWrappedPromise(queryRef: QueryReference) { export function unwrapQueryRef( queryRef: QueryReference -): [InternalQueryReference, () => QueryRefPromise] { - const internalQueryRef = queryRef[QUERY_REFERENCE_SYMBOL]; - - return [ - internalQueryRef, - () => - // There is a chance the query ref's promise has been updated in the time - // the original promise had been suspended. In that case, we want to use - // it instead of the older promise which may contain outdated data. - internalQueryRef.promise.status === "fulfilled" ? - internalQueryRef.promise - : getWrappedPromise(queryRef), - ]; +): InternalQueryReference { + return queryRef[QUERY_REFERENCE_SYMBOL]; } export function updateWrappedQueryRef( diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 1d57c7112b6..e02e0f77f57 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -44,7 +44,7 @@ import { import { useBackgroundQuery } from "../useBackgroundQuery"; import { useReadQuery } from "../useReadQuery"; import { ApolloProvider } from "../../context"; -import { unwrapQueryRef, QueryReference } from "../../cache/QueryReference"; +import { QueryReference } from "../../cache/QueryReference"; import { InMemoryCache } from "../../../cache"; import { SuspenseQueryHookFetchPolicy, @@ -643,7 +643,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await queryRef.toPromise(); expect(_result).toEqual({ data: { hello: "world 1" }, @@ -680,7 +680,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await queryRef.toPromise(); await waitFor(() => { expect(_result).toEqual({ @@ -721,7 +721,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await queryRef.toPromise(); await waitFor(() => { expect(_result).toMatchObject({ @@ -781,7 +781,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await queryRef.toPromise(); const resultSet = new Set(_result.data.results); const values = Array.from(resultSet).map((item) => item.value); @@ -842,7 +842,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await queryRef.toPromise(); const resultSet = new Set(_result.data.results); const values = Array.from(resultSet).map((item) => item.value); @@ -884,7 +884,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await queryRef.toPromise(); expect(_result).toEqual({ data: { hello: "from link" }, @@ -924,7 +924,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await queryRef.toPromise(); expect(_result).toEqual({ data: { hello: "from cache" }, @@ -971,7 +971,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await queryRef.toPromise(); expect(_result).toEqual({ data: { foo: "bar", hello: "from link" }, @@ -1011,7 +1011,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await queryRef.toPromise(); expect(_result).toEqual({ data: { hello: "from link" }, @@ -1054,7 +1054,7 @@ describe("useBackgroundQuery", () => { const [queryRef] = result.current; - const _result = await unwrapQueryRef(queryRef)[0].promise; + const _result = await queryRef.toPromise(); expect(_result).toEqual({ data: { hello: "from link" }, diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 4424c6329b7..96ae008360e 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -212,7 +212,7 @@ export function useBackgroundQuery< const [wrappedQueryRef, setWrappedQueryRef] = React.useState( wrapQueryRef(queryRef) ); - if (unwrapQueryRef(wrappedQueryRef)[0] !== queryRef) { + if (unwrapQueryRef(wrappedQueryRef) !== queryRef) { setWrappedQueryRef(wrapQueryRef(queryRef)); } if (queryRef.didChangeOptions(watchQueryOptions)) { diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 80111307ecd..96f8cc974fa 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -124,7 +124,7 @@ export function useLoadableQuery< TVariables > | null>(null); - const internalQueryRef = queryRef && unwrapQueryRef(queryRef)[0]; + const internalQueryRef = queryRef && unwrapQueryRef(queryRef); if (queryRef && internalQueryRef?.didChangeOptions(watchQueryOptions)) { const promise = internalQueryRef.applyOptions(watchQueryOptions); diff --git a/src/react/hooks/usePreloadedQueryHandlers.ts b/src/react/hooks/usePreloadedQueryHandlers.ts index 062c60f2cd6..9f514302650 100644 --- a/src/react/hooks/usePreloadedQueryHandlers.ts +++ b/src/react/hooks/usePreloadedQueryHandlers.ts @@ -25,7 +25,7 @@ export function usePreloadedQueryHandlers< queryRef: QueryReference ): UsePreloadedQueryHandlersResult { const [wrappedQueryRef, setWrappedQueryRef] = React.useState(queryRef); - const [internalQueryRef] = unwrapQueryRef(queryRef); + const internalQueryRef = unwrapQueryRef(queryRef); // To ensure we can support React transitions, this hook needs to manage the // queryRef state and apply React's state value immediately to the existing diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 664289e0220..398ceec2160 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -39,7 +39,7 @@ export interface UseReadQueryResult { export function useReadQuery( queryRef: QueryReference ): UseReadQueryResult { - const [internalQueryRef, getPromise] = React.useMemo( + const internalQueryRef = React.useMemo( () => unwrapQueryRef(queryRef), [queryRef] ); @@ -68,8 +68,8 @@ If you're using a queryRef produced by 'useBackgroundQuery' or 'useLoadableQuery }, [internalQueryRef] ), - getPromise, - getPromise + () => queryRef.toPromise(), + () => queryRef.toPromise() ); const result = __use(promise); From d3dc0b82c42125122e29824ed6f8fda7e7b07843 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:17:01 -0700 Subject: [PATCH 074/133] Return generic promise from toPromise --- src/react/cache/QueryReference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index b25e92aacd8..50a4f93f690 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -43,7 +43,7 @@ export interface QueryReference { readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; [PROMISE_SYMBOL]: QueryRefPromise; retain: () => DisposeFn; - toPromise(): QueryRefPromise; + toPromise(): Promise>; } interface InternalQueryReferenceOptions { From ea14e6efc70b01af4b89d69852005875cd1f8f01 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:21:06 -0700 Subject: [PATCH 075/133] Remove unneeded TVariables on unwrapQueryRef --- src/react/cache/QueryReference.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 50a4f93f690..9e0269adea4 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -73,8 +73,8 @@ export function getWrappedPromise(queryRef: QueryReference) { return queryRef[PROMISE_SYMBOL]; } -export function unwrapQueryRef( - queryRef: QueryReference +export function unwrapQueryRef( + queryRef: QueryReference ): InternalQueryReference { return queryRef[QUERY_REFERENCE_SYMBOL]; } From b5aef123b1f1b6f6b9ecae9a9a3a521fcb295bae Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:22:58 -0700 Subject: [PATCH 076/133] Remove unneded TVaraibles from queryRef arg to updateWrappedQueryRef --- src/react/cache/QueryReference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 9e0269adea4..7c8b18a4686 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -80,7 +80,7 @@ export function unwrapQueryRef( } export function updateWrappedQueryRef( - queryRef: QueryReference, + queryRef: QueryReference, promise: QueryRefPromise ) { queryRef[PROMISE_SYMBOL] = promise; From a1b834a4efc1949e3bebc9598756de27924ae076 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:25:33 -0700 Subject: [PATCH 077/133] Simplify internal FetchMoreOptions --- src/react/cache/QueryReference.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 7c8b18a4686..d68bbd51131 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -26,10 +26,9 @@ type Listener = (promise: QueryRefPromise) => void; type DisposeFn = () => void; -type FetchMoreOptions< - TData, - TVariables extends OperationVariables, -> = Parameters["fetchMore"]>[0]; +type FetchMoreOptions = Parameters< + ObservableQuery["fetchMore"] +>[0]; const QUERY_REFERENCE_SYMBOL: unique symbol = Symbol(); const PROMISE_SYMBOL: unique symbol = Symbol(); @@ -250,12 +249,8 @@ export class InternalQueryReference { return this.initiateFetch(this.observable.refetch(variables)); } - fetchMore(options: FetchMoreOptions) { - return this.initiateFetch( - this.observable.fetchMore( - options as FetchMoreQueryOptions - ) - ); + fetchMore(options: FetchMoreOptions) { + return this.initiateFetch(this.observable.fetchMore(options)); } private dispose() { From 2bb69e4d36aafbb25944d5e35dc423ceb94e927b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:26:43 -0700 Subject: [PATCH 078/133] Remove unneeded type cast in SuspenseCache --- src/react/cache/SuspenseCache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/cache/SuspenseCache.ts b/src/react/cache/SuspenseCache.ts index d835b8732cc..36641c76cb4 100644 --- a/src/react/cache/SuspenseCache.ts +++ b/src/react/cache/SuspenseCache.ts @@ -45,6 +45,6 @@ export class SuspenseCache { }); } - return ref.current as InternalQueryReference; + return ref.current; } } From 75ba7eabd03e593ad21a5c7ca1306cb04e9fe845 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:32:12 -0700 Subject: [PATCH 079/133] Remove unused import in QueryReference --- src/react/cache/QueryReference.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index d68bbd51131..0c7b7b37143 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -2,7 +2,6 @@ import { equal } from "@wry/equality"; import type { ApolloError, ApolloQueryResult, - FetchMoreQueryOptions, ObservableQuery, OperationVariables, WatchQueryOptions, From 76f5d62f12866ccf9bf5597685f62e5e41dcb343 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:40:20 -0700 Subject: [PATCH 080/133] Add tests that ensure usePreloadedQueryHandlers can attach to queryRefs from useLoadableQuery and useBackgroundQuery --- .../usePreloadedQueryHandlers.test.tsx | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) diff --git a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx index fa0f7d23d25..9657e73fa24 100644 --- a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx +++ b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx @@ -20,6 +20,9 @@ import { Suspense } from "react"; import { createQueryPreloader } from "../../query-preloader/createQueryPreloader"; import { ApolloProvider } from "../../context"; import userEvent from "@testing-library/user-event"; +import { QueryReference } from "../../cache/QueryReference"; +import { useBackgroundQuery } from "../useBackgroundQuery"; +import { useLoadableQuery } from "../useLoadableQuery"; test("refetches and resuspends when calling refetch", async () => { const { query, mocks: defaultMocks } = useSimpleCase(); @@ -731,3 +734,207 @@ test("`refetch` works with startTransition", async () => { expect(todo).toHaveTextContent("Clean room (completed)"); }); }); + +test("can attach handlers to queryRefs produced by useBackgroundQuery", async () => { + const { query, mocks: defaultMocks } = useSimpleCase(); + + const user = userEvent.setup(); + + const mocks = [ + defaultMocks[0], + { + request: { query }, + result: { data: { greeting: "Hello again" } }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

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

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryReference; + }) { + const { refetch } = usePreloadedQueryHandlers(queryRef); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return ; + } + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + render(, { + wrapper: ({ children }) => { + return ( + + {children} + + ); + }, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); From 4fed6427634b804dddb0ad76220a376618005079 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 17:50:00 -0700 Subject: [PATCH 081/133] Convert transition test to profiler --- .../usePreloadedQueryHandlers.test.tsx | 91 +++++++++++++------ 1 file changed, 64 insertions(+), 27 deletions(-) diff --git a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx index 9657e73fa24..be3a792a670 100644 --- a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx +++ b/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx @@ -659,13 +659,23 @@ test("`refetch` works with startTransition", async () => { cache: new InMemoryCache(), }); + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); + const preloadQuery = createQueryPreloader(client); const queryRef = preloadQuery(query, { variables: { id: "1" } }); function App() { + useTrackRenders(); const { refetch } = usePreloadedQueryHandlers(queryRef); const [isPending, startTransition] = React.useTransition(); + Profiler.mergeSnapshot({ isPending }); + return ( <> + ); + } + + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const [queryRef, { refetch }] = useBackgroundQuery(query); + + Profiler.mergeSnapshot({ useBackgroundQueryIsPending: isPending }); + + return ( + <> + + }> + + + + ); + } + + render(, { + wrapper: ({ children }) => { + return ( + + {children} + + ); + }, + }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch from parent"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: true, + usePreloadedQueryHandlersIsPending: false, + result: { + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + usePreloadedQueryHandlersIsPending: false, + result: { + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await act(() => user.click(screen.getByText("Refetch from child"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + usePreloadedQueryHandlersIsPending: true, + result: { + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + usePreloadedQueryHandlersIsPending: false, + result: { + data: { greeting: "You again?" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender(); +}); + test("can attach handlers to queryRefs produced by useBackgroundQuery", async () => { const { query, mocks: defaultMocks } = useSimpleCase(); diff --git a/src/react/hooks/usePreloadedQueryHandlers.ts b/src/react/hooks/usePreloadedQueryHandlers.ts index 9f514302650..45c0e36416b 100644 --- a/src/react/hooks/usePreloadedQueryHandlers.ts +++ b/src/react/hooks/usePreloadedQueryHandlers.ts @@ -1,11 +1,13 @@ import * as React from "rehackt"; import { - getWrappedPromise, unwrapQueryRef, updateWrappedQueryRef, wrapQueryRef, } from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; +import type { + QueryRefPromise, + QueryReference, +} from "../cache/QueryReference.js"; import type { OperationVariables } from "../../core/types.js"; import type { RefetchFunction, FetchMoreFunction } from "./useSuspenseQuery.js"; import type { FetchMoreQueryOptions } from "../../core/watchQueryOptions.js"; @@ -24,13 +26,22 @@ export function usePreloadedQueryHandlers< >( queryRef: QueryReference ): UsePreloadedQueryHandlersResult { + const [previousQueryRef, setPreviousQueryRef] = React.useState(queryRef); const [wrappedQueryRef, setWrappedQueryRef] = React.useState(queryRef); const internalQueryRef = unwrapQueryRef(queryRef); // To ensure we can support React transitions, this hook needs to manage the // queryRef state and apply React's state value immediately to the existing // queryRef since this hook doesn't return the queryRef directly - updateWrappedQueryRef(queryRef, getWrappedPromise(wrappedQueryRef)); + if (previousQueryRef !== queryRef) { + setPreviousQueryRef(queryRef); + setWrappedQueryRef(queryRef); + } else { + updateWrappedQueryRef( + queryRef, + wrappedQueryRef.toPromise() as QueryRefPromise + ); + } const refetch: RefetchFunction = React.useCallback( (variables) => { From e284c6fa19200a6183100d21297c2873731e7d2e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 18:42:00 -0700 Subject: [PATCH 083/133] Rename usePreloadedQueryHandlers to useQueryRefHandlers --- config/jest.config.js | 2 +- ....test.tsx => useQueryRefHandlers.test.tsx} | 20 +++++++++---------- src/react/hooks/index.ts | 4 ++-- ...ueryHandlers.ts => useQueryRefHandlers.ts} | 6 +++--- 4 files changed, 16 insertions(+), 16 deletions(-) rename src/react/hooks/__tests__/{usePreloadedQueryHandlers.test.tsx => useQueryRefHandlers.test.tsx} (97%) rename src/react/hooks/{usePreloadedQueryHandlers.ts => useQueryRefHandlers.ts} (93%) diff --git a/config/jest.config.js b/config/jest.config.js index 8a7337e4550..6851e2a6e06 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -36,7 +36,7 @@ const react17TestFileIgnoreList = [ "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", "src/react/hooks/__tests__/useLoadableQuery.test.tsx", - "src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx", + "src/react/hooks/__tests__/useQueryRefHandlers.test.tsx", "src/react/query-preloader/__tests__/createQueryPreloader.test.tsx", ]; diff --git a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx similarity index 97% rename from src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx rename to src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index 035c1701a19..f3ec11d88f5 100644 --- a/src/react/hooks/__tests__/usePreloadedQueryHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -14,7 +14,7 @@ import { useSimpleCase, useTrackRenders, } from "../../../testing/internal"; -import { usePreloadedQueryHandlers } from "../usePreloadedQueryHandlers"; +import { useQueryRefHandlers } from "../useQueryRefHandlers"; import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; import { Suspense } from "react"; import { createQueryPreloader } from "../../query-preloader/createQueryPreloader"; @@ -65,7 +65,7 @@ test("refetches and resuspends when calling refetch", async () => { function App() { useTrackRenders(); - const { refetch } = usePreloadedQueryHandlers(queryRef); + const { refetch } = useQueryRefHandlers(queryRef); return ( <> @@ -156,7 +156,7 @@ test("does not interfere with updates from useReadQuery", async () => { // 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 - usePreloadedQueryHandlers(queryRef); + useQueryRefHandlers(queryRef); return ( }> @@ -293,7 +293,7 @@ test('honors refetchWritePolicy set to "merge"', async () => { function App() { useTrackRenders(); - const { refetch } = usePreloadedQueryHandlers(queryRef); + const { refetch } = useQueryRefHandlers(queryRef); return ( <> @@ -427,7 +427,7 @@ test('honors refetchWritePolicy set to "overwrite"', async () => { function App() { useTrackRenders(); - const { refetch } = usePreloadedQueryHandlers(queryRef); + const { refetch } = useQueryRefHandlers(queryRef); return ( <> @@ -557,7 +557,7 @@ test('defaults refetchWritePolicy to "overwrite"', async () => { function App() { useTrackRenders(); - const { refetch } = usePreloadedQueryHandlers(queryRef); + const { refetch } = useQueryRefHandlers(queryRef); return ( <> @@ -671,7 +671,7 @@ test("`refetch` works with startTransition", async () => { function App() { useTrackRenders(); - const { refetch } = usePreloadedQueryHandlers(queryRef); + const { refetch } = useQueryRefHandlers(queryRef); const [isPending, startTransition] = React.useTransition(); Profiler.mergeSnapshot({ isPending }); @@ -816,7 +816,7 @@ test("works with startTransition on refetch from useBackgroundQuery and usePrelo }) { useTrackRenders(); const [isPending, startTransition] = React.useTransition(); - const { refetch } = usePreloadedQueryHandlers(queryRef); + const { refetch } = useQueryRefHandlers(queryRef); Profiler.mergeSnapshot({ usePreloadedQueryHandlersIsPending: isPending, @@ -989,7 +989,7 @@ test("can attach handlers to queryRefs produced by useBackgroundQuery", async () }: { queryRef: QueryReference; }) { - const { refetch } = usePreloadedQueryHandlers(queryRef); + const { refetch } = useQueryRefHandlers(queryRef); Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); return ; @@ -1088,7 +1088,7 @@ test("can attach handlers to queryRefs produced by useLoadableQuery", async () = }: { queryRef: QueryReference; }) { - const { refetch } = usePreloadedQueryHandlers(queryRef); + const { refetch } = useQueryRefHandlers(queryRef); Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); return ; diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 345ea719931..78fc82c61f4 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -16,8 +16,8 @@ export type { UseLoadableQueryResult, } from "./useLoadableQuery.js"; export { useLoadableQuery } from "./useLoadableQuery.js"; -export type { UsePreloadedQueryHandlersResult } from "./usePreloadedQueryHandlers.js"; -export { usePreloadedQueryHandlers } from "./usePreloadedQueryHandlers.js"; +export type { UseQueryRefHandlersResult } from "./useQueryRefHandlers.js"; +export { useQueryRefHandlers } from "./useQueryRefHandlers.js"; export type { UseReadQueryResult } from "./useReadQuery.js"; export { useReadQuery } from "./useReadQuery.js"; export { skipToken } from "./constants.js"; diff --git a/src/react/hooks/usePreloadedQueryHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts similarity index 93% rename from src/react/hooks/usePreloadedQueryHandlers.ts rename to src/react/hooks/useQueryRefHandlers.ts index 45c0e36416b..f99e8268e4b 100644 --- a/src/react/hooks/usePreloadedQueryHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -12,7 +12,7 @@ import type { OperationVariables } from "../../core/types.js"; import type { RefetchFunction, FetchMoreFunction } from "./useSuspenseQuery.js"; import type { FetchMoreQueryOptions } from "../../core/watchQueryOptions.js"; -export interface UsePreloadedQueryHandlersResult< +export interface UseQueryRefHandlersResult< TData = unknown, TVariables extends OperationVariables = OperationVariables, > { @@ -20,12 +20,12 @@ export interface UsePreloadedQueryHandlersResult< fetchMore: FetchMoreFunction; } -export function usePreloadedQueryHandlers< +export function useQueryRefHandlers< TData = unknown, TVariables extends OperationVariables = OperationVariables, >( queryRef: QueryReference -): UsePreloadedQueryHandlersResult { +): UseQueryRefHandlersResult { const [previousQueryRef, setPreviousQueryRef] = React.useState(queryRef); const [wrappedQueryRef, setWrappedQueryRef] = React.useState(queryRef); const internalQueryRef = unwrapQueryRef(queryRef); From 09d745164a163fbf10f71225e7459eee000a1229 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Fri, 8 Dec 2023 18:42:29 -0700 Subject: [PATCH 084/133] Rerun exports --- src/__tests__/__snapshots__/exports.ts.snap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 73e911c0d35..f67bdb6f92d 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -62,8 +62,8 @@ Array [ "useLazyQuery", "useLoadableQuery", "useMutation", - "usePreloadedQueryHandlers", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", @@ -279,8 +279,8 @@ Array [ "useLazyQuery", "useLoadableQuery", "useMutation", - "usePreloadedQueryHandlers", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", @@ -324,8 +324,8 @@ Array [ "useLazyQuery", "useLoadableQuery", "useMutation", - "usePreloadedQueryHandlers", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", From 8da55d27e77f0a4ebdbd0edd9cfc76f8a9b8a1c2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 10:51:19 -0700 Subject: [PATCH 085/133] Add render helpers in testing utils --- src/testing/internal/index.ts | 6 +++ src/testing/internal/renderHelpers.tsx | 71 ++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 src/testing/internal/renderHelpers.tsx diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index 92dfafd76e9..3f344f45e64 100644 --- a/src/testing/internal/index.ts +++ b/src/testing/internal/index.ts @@ -4,3 +4,9 @@ export { ObservableStream } from "./ObservableStream.js"; export type { SimpleCaseData, VariablesCaseData } from "./scenarios/index.js"; export { useSimpleCase, useVariablesCase } from "./scenarios/index.js"; + +export type { + RenderWithClientOptions, + RenderWithMocksOptions, +} from "./renderHelpers.js"; +export { renderWithClient, renderWithMocks } from "./renderHelpers.js"; diff --git a/src/testing/internal/renderHelpers.tsx b/src/testing/internal/renderHelpers.tsx new file mode 100644 index 00000000000..32ec44c9573 --- /dev/null +++ b/src/testing/internal/renderHelpers.tsx @@ -0,0 +1,71 @@ +import * as React from "react"; +import type { ReactElement } from "react"; +import { render } from "@testing-library/react"; +import type { Queries, RenderOptions, queries } from "@testing-library/react"; +import type { ApolloClient } from "../../core/index.js"; +import { ApolloProvider } from "../../react/index.js"; +import type { MockedProviderProps } from "../react/MockedProvider.js"; +import { MockedProvider } from "../react/MockedProvider.js"; + +export interface RenderWithClientOptions< + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +> extends RenderOptions { + client: ApolloClient; +} + +export function renderWithClient< + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +>( + ui: ReactElement, + { + client, + wrapper: Wrapper = React.Fragment, + ...renderOptions + }: RenderWithClientOptions +) { + return render(ui, { + ...renderOptions, + wrapper: ({ children }) => { + return ( + + {children} + + ); + }, + }); +} + +export interface RenderWithMocksOptions< + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +> extends RenderOptions, + MockedProviderProps {} + +export function renderWithMocks< + Q extends Queries = typeof queries, + Container extends Element | DocumentFragment = HTMLElement, + BaseElement extends Element | DocumentFragment = Container, +>( + ui: ReactElement, + { + mocks, + wrapper: Wrapper = React.Fragment, + ...renderOptions + }: RenderWithMocksOptions +) { + return render(ui, { + ...renderOptions, + wrapper: ({ children }) => { + return ( + + {children} + + ); + }, + }); +} From 26fb4ef4481a8d3922ba52db47720895ad71675a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 10:55:44 -0700 Subject: [PATCH 086/133] Use renderWithClient helper in useQueryRefHandlers --- .../__tests__/useQueryRefHandlers.test.tsx | 86 +++---------------- 1 file changed, 10 insertions(+), 76 deletions(-) diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index f3ec11d88f5..72077f8fb91 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -11,6 +11,7 @@ import { MockLink, MockedResponse } from "../../../testing"; import { SimpleCaseData, createProfiler, + renderWithClient, useSimpleCase, useTrackRenders, } from "../../../testing/internal"; @@ -18,7 +19,6 @@ import { useQueryRefHandlers } from "../useQueryRefHandlers"; import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; import { Suspense } from "react"; import { createQueryPreloader } from "../../query-preloader/createQueryPreloader"; -import { ApolloProvider } from "../../context"; import userEvent from "@testing-library/user-event"; import { QueryReference } from "../../cache/QueryReference"; import { useBackgroundQuery } from "../useBackgroundQuery"; @@ -77,15 +77,7 @@ test("refetches and resuspends when calling refetch", async () => { ); } - render(, { - wrapper: ({ children }) => { - return ( - - {children} - - ); - }, - }); + renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); @@ -165,15 +157,7 @@ test("does not interfere with updates from useReadQuery", async () => { ); } - const { rerender } = render(, { - wrapper: ({ children }) => { - return ( - - {children} - - ); - }, - }); + const { rerender } = renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); @@ -305,15 +289,7 @@ test('honors refetchWritePolicy set to "merge"', async () => { ); } - render(, { - wrapper: ({ children }) => { - return ( - - {children} - - ); - }, - }); + renderWithClient(, { client, wrapper: Profiler }); // initial render await Profiler.takeRender(); @@ -439,15 +415,7 @@ test('honors refetchWritePolicy set to "overwrite"', async () => { ); } - render(, { - wrapper: ({ children }) => { - return ( - - {children} - - ); - }, - }); + renderWithClient(, { client, wrapper: Profiler }); // initial render await Profiler.takeRender(); @@ -569,15 +537,7 @@ test('defaults refetchWritePolicy to "overwrite"', async () => { ); } - render(, { - wrapper: ({ children }) => { - return ( - - {children} - - ); - }, - }); + renderWithClient(, { client, wrapper: Profiler }); // initial render await Profiler.takeRender(); @@ -715,9 +675,7 @@ test("`refetch` works with startTransition", async () => { ); } - render(, { - wrapper: ({ children }) => {children}, - }); + render(, { wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); @@ -861,15 +819,7 @@ test("works with startTransition on refetch from useBackgroundQuery and usePrelo ); } - render(, { - wrapper: ({ children }) => { - return ( - - {children} - - ); - }, - }); + renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); @@ -1008,15 +958,7 @@ test("can attach handlers to queryRefs produced by useBackgroundQuery", async () ); } - render(, { - wrapper: ({ children }) => { - return ( - - {children} - - ); - }, - }); + renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); @@ -1108,15 +1050,7 @@ test("can attach handlers to queryRefs produced by useLoadableQuery", async () = ); } - render(, { - wrapper: ({ children }) => { - return ( - - {children} - - ); - }, - }); + renderWithClient(, { client, wrapper: Profiler }); // initial render await Profiler.takeRender(); From 558f0a15efe9aa6b0ab4137c3d29f01dba0733e3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 11:03:13 -0700 Subject: [PATCH 087/133] Calling retain on a disposed query ref does nothing --- src/react/cache/QueryReference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 78822463775..046d2904ac0 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -189,7 +189,7 @@ export class InternalQueryReference { let disposed = false; return () => { - if (disposed) { + if (disposed || this.disposed) { return; } From 6cb99e5aa9badfc5b03604484fba52976ae8a898 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 11:19:17 -0700 Subject: [PATCH 088/133] Fix query preloader test due to changes in API --- .../__tests__/createQueryPreloader.test.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 26732a5010f..a53f31d3001 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -191,18 +191,19 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { const queryRef = preloadQuery(query); const dispose = queryRef.retain(); - const { Profiler, rerender } = renderDefaultTestApp({ client, queryRef }); - - await Profiler.takeRender(); - await Profiler.takeRender(); - - await expect(Profiler).not.toRerender(); + await queryRef.toPromise(); dispose(); await wait(0); - rerender(); + function App() { + useReadQuery(queryRef); + + return null; + } + + const { rerender } = render(); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( @@ -211,7 +212,7 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { ) ); - rerender(); + rerender(); // Ensure re-rendering again only shows the warning once expect(console.warn).toHaveBeenCalledTimes(1); @@ -590,9 +591,9 @@ test("creates unique query refs when provided with a queryKey", async () => { const queryRef2 = preloadQuery(query); const queryRef3 = preloadQuery(query, { queryKey: 1 }); - const [unwrappedQueryRef1] = unwrapQueryRef(queryRef1); - const [unwrappedQueryRef2] = unwrapQueryRef(queryRef2); - const [unwrappedQueryRef3] = unwrapQueryRef(queryRef3); + const unwrappedQueryRef1 = unwrapQueryRef(queryRef1); + const unwrappedQueryRef2 = unwrapQueryRef(queryRef2); + const unwrappedQueryRef3 = unwrapQueryRef(queryRef3); expect(unwrappedQueryRef2).toBe(unwrappedQueryRef1); expect(unwrappedQueryRef3).not.toBe(unwrappedQueryRef1); From 4f3806a7c83558efe163f8d9d974b8070593a28b Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 11:40:59 -0700 Subject: [PATCH 089/133] Use renderHook for dispose test --- .../__tests__/createQueryPreloader.test.tsx | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index a53f31d3001..95be75ba3ef 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -27,7 +27,7 @@ import { useVariablesCase, } from "../../../testing/internal"; import { ApolloProvider } from "../../context"; -import { render } from "@testing-library/react"; +import { render, renderHook } from "@testing-library/react"; import { UseReadQueryResult, useReadQuery } from "../../hooks"; import { GraphQLError } from "graphql"; import { ErrorBoundary } from "react-error-boundary"; @@ -197,13 +197,7 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { await wait(0); - function App() { - useReadQuery(queryRef); - - return null; - } - - const { rerender } = render(); + const { rerender } = renderHook(() => useReadQuery(queryRef)); expect(console.warn).toHaveBeenCalledTimes(1); expect(console.warn).toHaveBeenCalledWith( @@ -212,7 +206,7 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { ) ); - rerender(); + rerender(); // Ensure re-rendering again only shows the warning once expect(console.warn).toHaveBeenCalledTimes(1); From 4d77d9bef49dc2099a0bf0e481fb198044867986 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 11:47:25 -0700 Subject: [PATCH 090/133] Warn again when passing a different disposed query ref to useReadQuery --- src/react/hooks/useReadQuery.ts | 9 +++-- .../__tests__/createQueryPreloader.test.tsx | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 398ceec2160..90ec812cc6b 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -47,14 +47,17 @@ export function useReadQuery( React.useEffect(() => internalQueryRef.retain(), [internalQueryRef]); if (__DEV__) { - const didWarnOnDisposedQueryRef = React.useRef(false); - if (!didWarnOnDisposedQueryRef.current && internalQueryRef.disposed) { + const didWarnOnDisposedQueryRef = React.useRef(void 0); + if ( + didWarnOnDisposedQueryRef.current !== internalQueryRef && + internalQueryRef.disposed + ) { invariant.warn(`'useReadQuery' was called with a disposed queryRef which means the query is no longer watched and cache updates will be missed. This occurs when calling 'dispose' while 'useReadQuery' is still mounted, either by calling 'dispose' too early, or because you are using React's strict mode and calling 'dispose' in a 'useEffect' cleanup function. If you're using a queryRef produced by 'useBackgroundQuery' or 'useLoadableQuery', this is a bug in Apollo Client. Please file an issue.`); - didWarnOnDisposedQueryRef.current = true; + didWarnOnDisposedQueryRef.current = internalQueryRef; } } diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 95be75ba3ef..e17eb74e680 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -212,6 +212,44 @@ test("useReadQuery warns when called with a disposed queryRef", async () => { expect(console.warn).toHaveBeenCalledTimes(1); }); +test("useReadQuery warns again when called with a different disposed query ref", async () => { + using _consoleSpy = spyOnConsole("warn"); + const { query } = useSimpleCase(); + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + maxUsageCount: 2, + }, + ]; + + const client = createDefaultClient(mocks); + + const preloadQuery = createQueryPreloader(client); + const queryRef1 = preloadQuery(query, { queryKey: 1 }); + const queryRef2 = preloadQuery(query, { queryKey: 2 }); + const dispose1 = queryRef1.retain(); + const dispose2 = queryRef2.retain(); + + await Promise.all([queryRef1.toPromise(), queryRef2.toPromise()]); + + dispose1(); + dispose2(); + + await wait(0); + + const { rerender } = renderHook(({ queryRef }) => useReadQuery(queryRef), { + initialProps: { queryRef: queryRef1 }, + }); + + expect(console.warn).toHaveBeenCalledTimes(1); + + rerender({ queryRef: queryRef2 }); + + expect(console.warn).toHaveBeenCalledTimes(2); +}); + test("reacts to cache updates", async () => { const { query, mocks } = useSimpleCase(); const client = createDefaultClient(mocks); From fc757b16f484bad2a97ef4555070e843a02cf9a6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 11:57:18 -0700 Subject: [PATCH 091/133] Add test to ensure auto dispose timer kicks in on preloading --- .../__tests__/createQueryPreloader.test.tsx | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index e17eb74e680..07360de6681 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -172,9 +172,29 @@ test("tears down the query when calling dispose", async () => { expect(client).not.toHaveSuspenseCacheEntryUsing(query); }); -test.todo( - "Auto disposes of the query ref if not retained within the given time" -); +test("Auto disposes of the query ref if not retained within the given time", async () => { + jest.useFakeTimers(); + const { query, mocks } = useSimpleCase(); + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + + // We don't start the dispose timer until the promise is initially resolved + // so we need to wait for it + jest.advanceTimersByTime(20); + await queryRef.toPromise(); + jest.advanceTimersByTime(30_000); + + const internalQueryRef = unwrapQueryRef(queryRef); + + expect(internalQueryRef.disposed).toBe(true); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + jest.useRealTimers(); +}); + test.todo( "useReadQuery auto-retains the queryRef and disposes of it when unmounted" ); From 51df7c2abe806441f1ea4541ee9d4c788852583c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 11:59:20 -0700 Subject: [PATCH 092/133] Add test to ensure configured auto dispose timer is honored --- .../__tests__/createQueryPreloader.test.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 07360de6681..22c6d5ee5b9 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -195,6 +195,40 @@ test("Auto disposes of the query ref if not retained within the given time", asy jest.useRealTimers(); }); +test("Honors configured auto dispose timer on the client", async () => { + jest.useFakeTimers(); + const { query, mocks } = useSimpleCase(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + defaultOptions: { + react: { + suspense: { + autoDisposeTimeoutMs: 5000, + }, + }, + }, + }); + + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + + // We don't start the dispose timer until the promise is initially resolved + // so we need to wait for it + jest.advanceTimersByTime(20); + await queryRef.toPromise(); + jest.advanceTimersByTime(5_000); + + const internalQueryRef = unwrapQueryRef(queryRef); + + expect(internalQueryRef.disposed).toBe(true); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + jest.useRealTimers(); +}); + test.todo( "useReadQuery auto-retains the queryRef and disposes of it when unmounted" ); From e755d82b65a13455d4fdab86e0c515cf03e08423 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 12:07:05 -0700 Subject: [PATCH 093/133] Add test to ensure useReadQuery auto retains the query ref --- .../__tests__/createQueryPreloader.test.tsx | 37 +++++++++++++++++-- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 22c6d5ee5b9..f662f2d147b 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -27,7 +27,7 @@ import { useVariablesCase, } from "../../../testing/internal"; import { ApolloProvider } from "../../context"; -import { render, renderHook } from "@testing-library/react"; +import { act, render, renderHook } from "@testing-library/react"; import { UseReadQueryResult, useReadQuery } from "../../hooks"; import { GraphQLError } from "graphql"; import { ErrorBoundary } from "react-error-boundary"; @@ -229,9 +229,38 @@ test("Honors configured auto dispose timer on the client", async () => { jest.useRealTimers(); }); -test.todo( - "useReadQuery auto-retains the queryRef and disposes of it when unmounted" -); +test("useReadQuery auto-retains the queryRef and disposes of it when unmounted", async () => { + jest.useFakeTimers(); + const { query, mocks } = useSimpleCase(); + + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + + const { unmount } = renderHook(() => useReadQuery(queryRef)); + + // We don't start the dispose timer until the promise is initially resolved + // so we need to wait for it + jest.advanceTimersByTime(20); + await act(() => queryRef.toPromise()); + jest.advanceTimersByTime(30_000); + + const internalQueryRef = unwrapQueryRef(queryRef); + + expect(internalQueryRef.disposed).toBe(false); + + jest.useRealTimers(); + + unmount(); + + await wait(0); + + expect(internalQueryRef.disposed).toBe(true); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + test.todo( "queryRef is not disposed when useReadQuery unmounts when manually retained" ); From 19e105693d1f86eddc48f9a9fd22fc5f207e0bf3 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 12:09:20 -0700 Subject: [PATCH 094/133] Add test to ensure manual retain and useReadQuery work correctly --- .../__tests__/createQueryPreloader.test.tsx | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index f662f2d147b..d2798e744ee 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -261,9 +261,30 @@ test("useReadQuery auto-retains the queryRef and disposes of it when unmounted", expect(client).not.toHaveSuspenseCacheEntryUsing(query); }); -test.todo( - "queryRef is not disposed when useReadQuery unmounts when manually retained" -); +test("unmounting useReadQuery does not auto dispose of the queryRef when manually retained", async () => { + const { query, mocks } = useSimpleCase(); + + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + const dispose = queryRef.retain(); + + const { unmount } = renderHook(() => useReadQuery(queryRef)); + + await act(() => queryRef.toPromise()); + + const internalQueryRef = unwrapQueryRef(queryRef); + + expect(internalQueryRef.disposed).toBe(false); + + unmount(); + await wait(0); + + expect(internalQueryRef.disposed).toBe(false); + + dispose(); +}); test("useReadQuery warns when called with a disposed queryRef", async () => { using _consoleSpy = spyOnConsole("warn"); From 7561ee92c259e1a3f7029b19cc4a1abac66f497c Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 12:47:34 -0700 Subject: [PATCH 095/133] Fix variables type errors when other options are present --- .../__tests__/createQueryPreloader.test.tsx | 57 +++++++++++++++++++ .../query-preloader/createQueryPreloader.ts | 16 ++++-- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index d2798e744ee..525eb8d95b9 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -996,6 +996,7 @@ describe.skip("type tests", () => { preloadQuery(query); preloadQuery(query, { variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); preloadQuery(query, { variables: { foo: "bar" } }); preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); }); @@ -1005,6 +1006,7 @@ describe.skip("type tests", () => { preloadQuery(query); preloadQuery(query, { variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); preloadQuery(query, { variables: { foo: "bar" } }); preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); }); @@ -1017,6 +1019,7 @@ describe.skip("type tests", () => { preloadQuery(query); preloadQuery(query, { variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); // @ts-expect-error unknown variables preloadQuery(query, { variables: { foo: "bar" } }); }); @@ -1028,6 +1031,8 @@ describe.skip("type tests", () => { // @ts-expect-error no variables option allowed preloadQuery(query, { variables: {} }); // @ts-expect-error no variables option allowed + preloadQuery(query, { returnPartialData: true, variables: {} }); + // @ts-expect-error no variables option allowed preloadQuery(query, { variables: { foo: "bar" } }); }); @@ -1037,13 +1042,22 @@ describe.skip("type tests", () => { preloadQuery(query); preloadQuery(query, { variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); preloadQuery(query, { variables: { limit: 10 } }); + preloadQuery(query, { returnPartialData: true, variables: { limit: 10 } }); preloadQuery(query, { variables: { // @ts-expect-error unknown variable foo: "bar", }, }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { variables: { limit: 10, @@ -1051,6 +1065,14 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + returnPartialData: true, + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); }); test("enforces required variables", () => { @@ -1061,8 +1083,18 @@ describe.skip("type tests", () => { preloadQuery(query); // @ts-expect-error empty variables preloadQuery(query, { variables: {} }); + // @ts-expect-error empty variables + preloadQuery(query, { returnPartialData: true, variables: {} }); preloadQuery(query, { variables: { id: "1" } }); + preloadQuery(query, { returnPartialData: true, variables: { id: "1" } }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { + returnPartialData: true, variables: { // @ts-expect-error unknown variable foo: "bar", @@ -1075,6 +1107,14 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + returnPartialData: true, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); }); test("requires variables with mixed TVariables", () => { @@ -1087,6 +1127,8 @@ describe.skip("type tests", () => { preloadQuery(query); // @ts-expect-error missing variables argument preloadQuery(query, { variables: {} }); + // @ts-expect-error missing variables argument + preloadQuery(query, { returnPartialData: true, variables: {} }); preloadQuery(query, { variables: { id: "1" } }); // @ts-expect-error missing required variable preloadQuery(query, { variables: { language: "en" } }); @@ -1098,6 +1140,21 @@ describe.skip("type tests", () => { }, }); preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, variables: { id: "1", // @ts-expect-error unknown variable diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 0007da0859b..ee768b726ea 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -48,8 +48,14 @@ type PreloadQueryOptionsArg< > = [TVariables] extends [never] ? [options?: PreloadQueryOptions & TOptions] : {} extends OnlyRequiredProperties ? - [options?: PreloadQueryOptions> & TOptions] -: [options: PreloadQueryOptions> & TOptions]; + [ + options?: PreloadQueryOptions> & + Omit, + ] +: [ + options: PreloadQueryOptions> & + Omit, + ]; export function createQueryPreloader(client: ApolloClient) { const suspenseCache = getSuspenseCache(client); @@ -57,7 +63,7 @@ export function createQueryPreloader(client: ApolloClient) { function preloadQuery< TData, TVariables extends OperationVariables, - TOptions extends Omit, "variables">, + TOptions extends Omit, >( query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg, TOptions> @@ -76,7 +82,7 @@ export function createQueryPreloader(client: ApolloClient) { TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: PreloadQueryOptions & { + options: PreloadQueryOptions> & { returnPartialData: true; errorPolicy: "ignore" | "all"; } @@ -97,7 +103,7 @@ export function createQueryPreloader(client: ApolloClient) { TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: PreloadQueryOptions & { + options: PreloadQueryOptions> & { returnPartialData: true; } ): QueryReference, TVariables>; From 74d248cb3f1b549f9951d0f096e6bd72b096f74a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 12:51:20 -0700 Subject: [PATCH 096/133] Use Record for never variables type --- .../query-preloader/__tests__/createQueryPreloader.test.tsx | 2 -- src/react/query-preloader/createQueryPreloader.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 525eb8d95b9..9ffc830ee6c 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -1028,9 +1028,7 @@ describe.skip("type tests", () => { const query: TypedDocumentNode<{ greeting: string }, never> = gql``; preloadQuery(query); - // @ts-expect-error no variables option allowed preloadQuery(query, { variables: {} }); - // @ts-expect-error no variables option allowed preloadQuery(query, { returnPartialData: true, variables: {} }); // @ts-expect-error no variables option allowed preloadQuery(query, { variables: { foo: "bar" } }); diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index ee768b726ea..d0e5c44dfe2 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -21,7 +21,7 @@ import type { CacheKey } from "../cache/types.js"; import type { NoInfer } from "../index.js"; type VariablesOption = - [TVariables] extends [never] ? { variables?: never } + [TVariables] extends [never] ? { variables?: Record } : {} extends OnlyRequiredProperties ? { variables?: TVariables } : { variables: TVariables }; From 482dc88cec864f4fbf7a00cac0cbde44b2982cfc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 12:53:34 -0700 Subject: [PATCH 097/133] Use Record in simple variables case --- src/testing/internal/scenarios/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts index 3044c90da85..7812ad3d9ac 100644 --- a/src/testing/internal/scenarios/index.ts +++ b/src/testing/internal/scenarios/index.ts @@ -7,7 +7,7 @@ export interface SimpleCaseData { } export function useSimpleCase() { - const query: TypedDocumentNode = gql` + const query: TypedDocumentNode> = gql` query GreetingQuery { greeting } From a768c9e8257775d460131d13fbbfa4d425733666 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 14:17:05 -0700 Subject: [PATCH 098/133] Add a couple more type tests for other options with variables --- .../query-preloader/__tests__/createQueryPreloader.test.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 9ffc830ee6c..00a5f5edbcc 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -1022,6 +1022,8 @@ describe.skip("type tests", () => { preloadQuery(query, { returnPartialData: true, variables: {} }); // @ts-expect-error unknown variables preloadQuery(query, { variables: { foo: "bar" } }); + // @ts-expect-error unknown variables + preloadQuery(query, { returnPartialData: true, variables: { foo: "bar" } }); }); test("does not allow variables when TVariables is `never`", () => { @@ -1030,8 +1032,10 @@ describe.skip("type tests", () => { preloadQuery(query); preloadQuery(query, { variables: {} }); preloadQuery(query, { returnPartialData: true, variables: {} }); - // @ts-expect-error no variables option allowed + // @ts-expect-error no variables allowed preloadQuery(query, { variables: { foo: "bar" } }); + // @ts-expect-error no variables allowed + preloadQuery(query, { returnPartialData: true, variables: { foo: "bar" } }); }); test("optional variables are optional", () => { From 5abedcd1e932048ea66182b371334ff46ae5dfd2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 14:46:53 -0700 Subject: [PATCH 099/133] Update type tests to test for explicit type args --- .../__tests__/createQueryPreloader.test.tsx | 265 ++++++++++++++++-- 1 file changed, 237 insertions(+), 28 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index 00a5f5edbcc..a90b2462b7d 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -1001,58 +1001,130 @@ describe.skip("type tests", () => { preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); }); - test("variables are optional and can be anything with unspecified TVariables on a TypedDocumentNode", () => { - const query: TypedDocumentNode<{ greeting: string }> = gql``; + test("variables are optional and can be anything with unspecified TVariables", () => { + type Data = { greeting: string }; + const query: TypedDocumentNode = gql``; preloadQuery(query); + preloadQuery(query); preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: {} }); preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); preloadQuery(query, { variables: { foo: "bar" } }); + preloadQuery(query, { variables: { foo: "bar" } }); preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); + preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); }); test("variables are optional when TVariables are empty", () => { - const query: TypedDocumentNode< - { greeting: string }, - Record - > = gql``; + type Data = { greeting: string }; + type Variables = Record; + const query: TypedDocumentNode = gql``; preloadQuery(query); + preloadQuery(query); preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: {} }); preloadQuery(query, { returnPartialData: true, variables: {} }); - // @ts-expect-error unknown variables - preloadQuery(query, { variables: { foo: "bar" } }); - // @ts-expect-error unknown variables - preloadQuery(query, { returnPartialData: true, variables: { foo: "bar" } }); + preloadQuery(query, { + returnPartialData: true, + variables: {}, + }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variables + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variables + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variables + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variables + foo: "bar", + }, + }); }); test("does not allow variables when TVariables is `never`", () => { - const query: TypedDocumentNode<{ greeting: string }, never> = gql``; + type Data = { greeting: string }; + const query: TypedDocumentNode = gql``; preloadQuery(query); + preloadQuery(query); preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: {} }); preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, { + returnPartialData: true, + variables: {}, + }); // @ts-expect-error no variables allowed preloadQuery(query, { variables: { foo: "bar" } }); // @ts-expect-error no variables allowed - preloadQuery(query, { returnPartialData: true, variables: { foo: "bar" } }); + preloadQuery(query, { variables: { foo: "bar" } }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error no variables allowed + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error no variables allowed + foo: "bar", + }, + }); }); test("optional variables are optional", () => { - const query: TypedDocumentNode<{ posts: string[] }, { limit?: number }> = - gql``; + type Data = { posts: string[] }; + type Variables = { limit?: number }; + const query: TypedDocumentNode = gql``; preloadQuery(query); + preloadQuery(query); preloadQuery(query, { variables: {} }); + preloadQuery(query, { variables: {} }); preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, { + returnPartialData: true, + variables: {}, + }); preloadQuery(query, { variables: { limit: 10 } }); + preloadQuery(query, { variables: { limit: 10 } }); preloadQuery(query, { returnPartialData: true, variables: { limit: 10 } }); + preloadQuery(query, { + returnPartialData: true, + variables: { limit: 10 }, + }); preloadQuery(query, { variables: { // @ts-expect-error unknown variable foo: "bar", }, }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { returnPartialData: true, variables: { @@ -1060,6 +1132,13 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { variables: { limit: 10, @@ -1067,6 +1146,13 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { returnPartialData: true, variables: { @@ -1075,26 +1161,62 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + returnPartialData: true, + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); }); test("enforces required variables", () => { - const query: TypedDocumentNode<{ character: string }, { id: string }> = - gql``; + type Data = { character: string }; + type Variables = { id: string }; + const query: TypedDocumentNode = gql``; // @ts-expect-error missing variables option preloadQuery(query); - // @ts-expect-error empty variables - preloadQuery(query, { variables: {} }); - // @ts-expect-error empty variables - preloadQuery(query, { returnPartialData: true, variables: {} }); + // @ts-expect-error missing variables option + preloadQuery(query); + preloadQuery(query, { + // @ts-expect-error empty variables + variables: {}, + }); + preloadQuery(query, { + // @ts-expect-error empty variables + variables: {}, + }); + preloadQuery(query, { + returnPartialData: true, + // @ts-expect-error empty variables + variables: {}, + }); + preloadQuery(query, { + returnPartialData: true, + // @ts-expect-error empty variables + variables: {}, + }); preloadQuery(query, { variables: { id: "1" } }); + preloadQuery(query, { variables: { id: "1" } }); preloadQuery(query, { returnPartialData: true, variables: { id: "1" } }); + preloadQuery(query, { + returnPartialData: true, + variables: { id: "1" }, + }); preloadQuery(query, { variables: { // @ts-expect-error unknown variable foo: "bar", }, }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { returnPartialData: true, variables: { @@ -1102,6 +1224,13 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { variables: { id: "1", @@ -1109,6 +1238,21 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { returnPartialData: true, variables: { @@ -1120,27 +1264,62 @@ describe.skip("type tests", () => { }); test("requires variables with mixed TVariables", () => { - const query: TypedDocumentNode< - { character: string }, - { id: string; language?: string } - > = gql``; + type Data = { character: string }; + type Variables = { id: string; language?: string }; + const query: TypedDocumentNode = gql``; // @ts-expect-error missing variables argument preloadQuery(query); // @ts-expect-error missing variables argument - preloadQuery(query, { variables: {} }); + preloadQuery(query); // @ts-expect-error missing variables argument - preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, {}); + // @ts-expect-error missing variables argument + preloadQuery(query, {}); + preloadQuery(query, { + // @ts-expect-error missing required variables + variables: {}, + }); + preloadQuery(query, { + // @ts-expect-error missing required variables + variables: {}, + }); + preloadQuery(query, { + returnPartialData: true, + // @ts-expect-error missing required variables + variables: {}, + }); + preloadQuery(query, { + returnPartialData: true, + // @ts-expect-error missing required variables + variables: {}, + }); preloadQuery(query, { variables: { id: "1" } }); - // @ts-expect-error missing required variable - preloadQuery(query, { variables: { language: "en" } }); + preloadQuery(query, { variables: { id: "1" } }); + preloadQuery(query, { + // @ts-expect-error missing required variable + variables: { language: "en" }, + }); + preloadQuery(query, { + // @ts-expect-error missing required variable + variables: { language: "en" }, + }); preloadQuery(query, { variables: { id: "1", language: "en" } }); + preloadQuery(query, { + variables: { id: "1", language: "en" }, + }); preloadQuery(query, { variables: { // @ts-expect-error unknown variable foo: "bar", }, }); + preloadQuery(query, { + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { returnPartialData: true, variables: { @@ -1148,6 +1327,13 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { variables: { id: "1", @@ -1155,6 +1341,13 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { returnPartialData: true, variables: { @@ -1163,6 +1356,14 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + returnPartialData: true, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); preloadQuery(query, { variables: { id: "1", @@ -1171,6 +1372,14 @@ describe.skip("type tests", () => { foo: "bar", }, }); + preloadQuery(query, { + variables: { + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); }); test("returns QueryReference when TData cannot be inferred", () => { From bf2da7590f2e48884d123e1df7e41a8c049e64a6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 15:09:40 -0700 Subject: [PATCH 100/133] Add warning when calling retain on a disposed queryRef --- src/react/cache/QueryReference.ts | 11 ++++++ .../cache/__tests__/QueryReference.test.ts | 38 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 src/react/cache/__tests__/QueryReference.test.ts diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index 046d2904ac0..e9f0d5bb466 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -18,6 +18,7 @@ import { import type { QueryKey } from "./types.js"; import type { useBackgroundQuery, useReadQuery } from "../hooks/index.js"; import { wrapPromiseWithState } from "../../utilities/index.js"; +import { invariant } from "../../utilities/globals/index.js"; /** @internal */ export type QueryRefPromise = PromiseWithState>; @@ -188,6 +189,16 @@ export class InternalQueryReference { clearTimeout(this.autoDisposeTimeoutId); let disposed = false; + if (__DEV__) { + if (this.disposed) { + invariant.warn( + `'retain' was called on a disposed queryRef which results in a no-op. Please recreate the queryRef by calling 'preloadQuery' again. + +If you're seeing this warning for a queryRef produced by 'useBackgroundQuery' or 'useLoadableQuery', this is a bug in Apollo Client. Please file an issue.` + ); + } + } + return () => { if (disposed || this.disposed) { return; diff --git a/src/react/cache/__tests__/QueryReference.test.ts b/src/react/cache/__tests__/QueryReference.test.ts new file mode 100644 index 00000000000..a35d3138e72 --- /dev/null +++ b/src/react/cache/__tests__/QueryReference.test.ts @@ -0,0 +1,38 @@ +import { ApolloClient, InMemoryCache } from "../../../core"; +import { MockLink, MockedResponse, wait } from "../../../testing"; +import { + SimpleCaseData, + spyOnConsole, + useSimpleCase, +} from "../../../testing/internal"; +import { InternalQueryReference } from "../QueryReference"; + +function createDefaultClient(mocks: MockedResponse[]) { + return new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); +} + +test("warns when calling `retain` on a disposed query ref", async () => { + using _consoleSpy = spyOnConsole("warn"); + const { query, mocks } = useSimpleCase(); + const client = createDefaultClient(mocks); + const observable = client.watchQuery>({ + query, + }); + + const queryRef = new InternalQueryReference(observable, {}); + const dispose = queryRef.retain(); + + dispose(); + + await wait(0); + + queryRef.retain(); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining("'retain' was called on a disposed queryRef") + ); +}); From ee38f4c400684c97399c9bcb9db204f645dbb7c6 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 15:10:12 -0700 Subject: [PATCH 101/133] Add any to ObservableQuery type on constructor in InternalQueryReference --- src/react/cache/QueryReference.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/cache/QueryReference.ts b/src/react/cache/QueryReference.ts index e9f0d5bb466..0ac99708b5c 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/cache/QueryReference.ts @@ -119,7 +119,7 @@ export class InternalQueryReference { private references = 0; constructor( - observable: ObservableQuery, + observable: ObservableQuery, options: InternalQueryReferenceOptions ) { this.handleNext = this.handleNext.bind(this); From 29fce006d613b78ff415a6fbf5d401df6c9b6602 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 15:14:13 -0700 Subject: [PATCH 102/133] Tweak warning message in useReadQuery --- src/react/hooks/useReadQuery.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 90ec812cc6b..4abbf1288b5 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -52,11 +52,9 @@ export function useReadQuery( didWarnOnDisposedQueryRef.current !== internalQueryRef && internalQueryRef.disposed ) { - invariant.warn(`'useReadQuery' was called with a disposed queryRef which means the query is no longer watched and cache updates will be missed. + invariant.warn(`'useReadQuery' was called with a disposed queryRef which means the query is no longer watched and cache updates will be missed. This may occur when calling 'dispose' before passing the queryRef to 'useReadQuery'. -This occurs when calling 'dispose' while 'useReadQuery' is still mounted, either by calling 'dispose' too early, or because you are using React's strict mode and calling 'dispose' in a 'useEffect' cleanup function. - -If you're using a queryRef produced by 'useBackgroundQuery' or 'useLoadableQuery', this is a bug in Apollo Client. Please file an issue.`); +If you're seeing this warning for a queryRef produced by 'useBackgroundQuery' or 'useLoadableQuery', this is a bug in Apollo Client. Please file an issue.`); didWarnOnDisposedQueryRef.current = internalQueryRef; } } From 005c01313298fdcd41f2af432e481fb059bfaad2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 15:17:48 -0700 Subject: [PATCH 103/133] Only retain the queryRef if not already disposed --- src/react/hooks/useReadQuery.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 4abbf1288b5..075ce684ee7 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -44,7 +44,11 @@ export function useReadQuery( [queryRef] ); - React.useEffect(() => internalQueryRef.retain(), [internalQueryRef]); + React.useEffect(() => { + if (!internalQueryRef.disposed) { + return internalQueryRef.retain(); + } + }, [internalQueryRef]); if (__DEV__) { const didWarnOnDisposedQueryRef = React.useRef(void 0); From 6ce3304fa79ad456c747367591d224acb9444ba0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 15:19:57 -0700 Subject: [PATCH 104/133] Add test to ensure disposing of query after mounting useReadQuery works --- .../__tests__/createQueryPreloader.test.tsx | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index a90b2462b7d..fb265dca975 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -286,6 +286,34 @@ test("unmounting useReadQuery does not auto dispose of the queryRef when manuall dispose(); }); +test("manually disposing of the queryRef after mounting useReadQuery does not dispose of the queryRef until unmount", async () => { + const { query, mocks } = useSimpleCase(); + + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + const dispose = queryRef.retain(); + + const { unmount } = renderHook(() => useReadQuery(queryRef)); + + await act(() => queryRef.toPromise()); + + const internalQueryRef = unwrapQueryRef(queryRef); + + expect(internalQueryRef.disposed).toBe(false); + + dispose(); + await wait(0); + + expect(internalQueryRef.disposed).toBe(false); + + unmount(); + await wait(0); + + expect(internalQueryRef.disposed).toBe(true); +}); + test("useReadQuery warns when called with a disposed queryRef", async () => { using _consoleSpy = spyOnConsole("warn"); const { query, mocks } = useSimpleCase(); From 62b6e9c86ee31d2273a300134b15cbe5d322c3e4 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 15:53:35 -0700 Subject: [PATCH 105/133] Add paginated case to scenarios --- src/testing/internal/index.ts | 14 ++++++-- src/testing/internal/scenarios/index.ts | 46 ++++++++++++++++++++++++- 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index 3f344f45e64..bf2faaec83f 100644 --- a/src/testing/internal/index.ts +++ b/src/testing/internal/index.ts @@ -2,8 +2,18 @@ export * from "./profile/index.js"; export * from "./disposables/index.js"; export { ObservableStream } from "./ObservableStream.js"; -export type { SimpleCaseData, VariablesCaseData } from "./scenarios/index.js"; -export { useSimpleCase, useVariablesCase } from "./scenarios/index.js"; +export type { + SimpleCaseData, + PaginatedCaseData, + PaginatedCaseVariables, + VariablesCaseData, + VariablesCaseVariables, +} from "./scenarios/index.js"; +export { + useSimpleCase, + useVariablesCase, + usePaginatedCase, +} from "./scenarios/index.js"; export type { RenderWithClientOptions, diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts index 7812ad3d9ac..13bb4da1263 100644 --- a/src/testing/internal/scenarios/index.ts +++ b/src/testing/internal/scenarios/index.ts @@ -1,4 +1,4 @@ -import { gql } from "../../../core/index.js"; +import { ApolloLink, Observable, gql } from "../../../core/index.js"; import type { TypedDocumentNode } from "../../../core/index.js"; import type { MockedResponse } from "../../core/index.js"; @@ -57,3 +57,47 @@ export function useVariablesCase() { return { mocks, query }; } + +interface Letter { + letter: string; + position: number; +} + +export interface PaginatedCaseData { + letters: Letter[]; +} + +export interface PaginatedCaseVariables { + limit?: number; + offset?: number; +} + +export function usePaginatedCase() { + const query: TypedDocumentNode = + gql` + query letters($limit: Int, $offset: Int) { + letters(limit: $limit) { + letter + position + } + } + `; + + const data = "ABCDEFGHIJKLMNOPQRSTUV" + .split("") + .map((letter, index) => ({ letter, position: index + 1 })); + + const link = new ApolloLink((operation) => { + const { offset = 0, limit = 2 } = operation.variables; + const letters = data.slice(offset, offset + limit); + + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { letters } }); + observer.complete(); + }, 10); + }); + }); + + return { query, link }; +} From 2c9dec2578512ad779991ca820e1ff7f089ca757 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 15:57:06 -0700 Subject: [PATCH 106/133] Slight test reorg --- .../__tests__/useQueryRefHandlers.test.tsx | 96 +++++++++---------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index 72077f8fb91..41f64594a5d 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -24,19 +24,8 @@ import { QueryReference } from "../../cache/QueryReference"; import { useBackgroundQuery } from "../useBackgroundQuery"; import { useLoadableQuery } from "../useLoadableQuery"; -test("refetches and resuspends when calling refetch", async () => { - const { query, mocks: defaultMocks } = useSimpleCase(); - - const user = userEvent.setup(); - - const mocks = [ - defaultMocks[0], - { - request: { query }, - result: { data: { greeting: "Hello again" } }, - delay: 20, - }, - ]; +test("does not interfere with updates from useReadQuery", async () => { + const { query, mocks } = useSimpleCase(); const client = new ApolloClient({ cache: new InMemoryCache(), @@ -58,6 +47,7 @@ test("refetches and resuspends when calling refetch", async () => { } function ReadQueryHook() { + useTrackRenders(); Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); return null; @@ -65,19 +55,19 @@ test("refetches and resuspends when calling refetch", async () => { function App() { useTrackRenders(); - const { refetch } = useQueryRefHandlers(queryRef); + // We can ignore the return result here since we are testing the mechanics + // of this hook to ensure it doesn't interfere with the updates from + // useReadQuery + useQueryRefHandlers(queryRef); return ( - <> - - }> - - - + }> + + ); } - renderWithClient(, { client, wrapper: Profiler }); + const { rerender } = renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); @@ -95,17 +85,25 @@ test("refetches and resuspends when calling refetch", async () => { }); } - await act(() => user.click(screen.getByText("Refetch"))); + client.writeQuery({ query, data: { greeting: "Hello again" } }); { - const { renderedComponents } = await Profiler.takeRender(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); } + rerender(); + { - const { snapshot } = await Profiler.takeRender(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); expect(snapshot.result).toEqual({ data: { greeting: "Hello again" }, error: undefined, @@ -114,8 +112,19 @@ test("refetches and resuspends when calling refetch", async () => { } }); -test("does not interfere with updates from useReadQuery", async () => { - const { query, mocks } = useSimpleCase(); +test("refetches and resuspends when calling refetch", async () => { + const { query, mocks: defaultMocks } = useSimpleCase(); + + const user = userEvent.setup(); + + const mocks = [ + defaultMocks[0], + { + request: { query }, + result: { data: { greeting: "Hello again" } }, + delay: 20, + }, + ]; const client = new ApolloClient({ cache: new InMemoryCache(), @@ -137,7 +146,6 @@ test("does not interfere with updates from useReadQuery", async () => { } function ReadQueryHook() { - useTrackRenders(); Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); return null; @@ -145,19 +153,19 @@ test("does not interfere with updates from useReadQuery", async () => { function App() { useTrackRenders(); - // We can ignore the return result here since we are testing the mechanics - // of this hook to ensure it doesn't interfere with the updates from - // useReadQuery - useQueryRefHandlers(queryRef); + const { refetch } = useQueryRefHandlers(queryRef); return ( - }> - - + <> + + }> + + + ); } - const { rerender } = renderWithClient(, { client, wrapper: Profiler }); + renderWithClient(, { client, wrapper: Profiler }); { const { renderedComponents } = await Profiler.takeRender(); @@ -175,25 +183,17 @@ test("does not interfere with updates from useReadQuery", async () => { }); } - client.writeQuery({ query, data: { greeting: "Hello again" } }); + await act(() => user.click(screen.getByText("Refetch"))); { - const { snapshot, renderedComponents } = await Profiler.takeRender(); + const { renderedComponents } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([ReadQueryHook]); - expect(snapshot.result).toEqual({ - data: { greeting: "Hello again" }, - error: undefined, - networkStatus: NetworkStatus.ready, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); } - rerender(); - { - const { snapshot, renderedComponents } = await Profiler.takeRender(); + const { snapshot } = await Profiler.takeRender(); - expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); expect(snapshot.result).toEqual({ data: { greeting: "Hello again" }, error: undefined, From 9b60a741ef1453dc9e46429ab6ef5311cc246141 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 15:58:17 -0700 Subject: [PATCH 107/133] Small test description update --- src/react/hooks/__tests__/useQueryRefHandlers.test.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index 41f64594a5d..fd107916abc 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -730,7 +730,7 @@ test("`refetch` works with startTransition", async () => { await expect(Profiler).not.toRerender(); }); -test("works with startTransition on refetch from useBackgroundQuery and usePreloadedQueryHandlers", async () => { +test("`refetch` works with startTransition from useBackgroundQuery and usePreloadedQueryHandlers", async () => { const { query, mocks: defaultMocks } = useSimpleCase(); const user = userEvent.setup(); @@ -904,7 +904,7 @@ test("works with startTransition on refetch from useBackgroundQuery and usePrelo await expect(Profiler).not.toRerender(); }); -test("can attach handlers to queryRefs produced by useBackgroundQuery", async () => { +test("refetches from queryRefs produced by useBackgroundQuery", async () => { const { query, mocks: defaultMocks } = useSimpleCase(); const user = userEvent.setup(); @@ -995,7 +995,7 @@ test("can attach handlers to queryRefs produced by useBackgroundQuery", async () } }); -test("can attach handlers to queryRefs produced by useLoadableQuery", async () => { +test("refetches from queryRefs produced by useLoadableQuery", async () => { const { query, mocks: defaultMocks } = useSimpleCase(); const user = userEvent.setup(); From 1bc7cc8e46b7cb7a8eab552a58469f2df3bfcf22 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 16:25:46 -0700 Subject: [PATCH 108/133] Add tests to ensure fetchMore works with useQueryRefHandlers --- .../__tests__/useQueryRefHandlers.test.tsx | 301 ++++++++++++++++++ 1 file changed, 301 insertions(+) diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index fd107916abc..16b02d24e99 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -9,9 +9,11 @@ import { } from "../../../core"; import { MockLink, MockedResponse } from "../../../testing"; import { + PaginatedCaseData, SimpleCaseData, createProfiler, renderWithClient, + usePaginatedCase, useSimpleCase, useTrackRenders, } from "../../../testing/internal"; @@ -23,6 +25,7 @@ import userEvent from "@testing-library/user-event"; import { QueryReference } from "../../cache/QueryReference"; import { useBackgroundQuery } from "../useBackgroundQuery"; import { useLoadableQuery } from "../useLoadableQuery"; +import { concatPagination } from "../../../utilities"; test("does not interfere with updates from useReadQuery", async () => { const { query, mocks } = useSimpleCase(); @@ -1091,3 +1094,301 @@ test("refetches from queryRefs produced by useLoadableQuery", async () => { }); } }); + +test("resuspends when calling `fetchMore`", async () => { + const { query, link } = usePaginatedCase(); + + const user = userEvent.setup(); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

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

Loading

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

Loading

; + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { fetchMore } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + + + + ); + } + + const queryRef = preloadQuery(query); + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Load next"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); From 41d0123833774244735af4303f8ba636d5a4af5a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 16:31:52 -0700 Subject: [PATCH 109/133] Add tests to ensure useQueryRefHandlers can call fetchMore from uBQ and uLQ --- .../__tests__/useQueryRefHandlers.test.tsx | 192 ++++++++++++++++++ 1 file changed, 192 insertions(+) diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index 16b02d24e99..06ad05a8664 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -1392,3 +1392,195 @@ test("properly uses cache field policies when calling `fetchMore` without `updat }); } }); + +test("paginates from queryRefs produced by useBackgroundQuery", async () => { + const { query, link } = usePaginatedCase(); + + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

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

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryReference; + }) { + useTrackRenders(); + const { fetchMore } = useQueryRefHandlers(queryRef); + + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return ( + + ); + } + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Load next"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); From 516c24ed9d8a2fad056a55aeb05cf07c2f4be524 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 16:41:54 -0700 Subject: [PATCH 110/133] Add test to ensure fetchMore works with startTransition --- .../__tests__/useQueryRefHandlers.test.tsx | 300 ++++++++++++++++++ 1 file changed, 300 insertions(+) diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index 06ad05a8664..491bb9cacc9 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -1584,3 +1584,303 @@ test("paginates from queryRefs produced by useLoadableQuery", async () => { }); } }); + +test("`fetchMore` works with startTransition", async () => { + const { query, link } = usePaginatedCase(); + + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

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

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryReference; + }) { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const { fetchMore } = useQueryRefHandlers(queryRef); + + Profiler.mergeSnapshot({ + useQueryRefHandlersIsPending: isPending, + result: useReadQuery(queryRef), + }); + + return ( + + ); + } + + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query); + + Profiler.mergeSnapshot({ useBackgroundQueryIsPending: isPending }); + + return ( + <> + + }> + + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Paginate from parent"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: true, + useQueryRefHandlersIsPending: false, + result: { + data: { + letters: [ + { letter: "A", position: 1 }, + { letter: "B", position: 2 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + useQueryRefHandlersIsPending: false, + result: { + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await act(() => user.click(screen.getByText("Paginate from child"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + useQueryRefHandlersIsPending: true, + result: { + data: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + useBackgroundQueryIsPending: false, + useQueryRefHandlersIsPending: false, + result: { + data: { + letters: [ + { letter: "E", position: 5 }, + { letter: "F", position: 6 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender(); +}); From 23ca5ff229c9095c9ec4cc0677ca810430b783cc Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 16:44:55 -0700 Subject: [PATCH 111/133] Minor tweak to simplify test --- src/react/cache/__tests__/QueryReference.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/react/cache/__tests__/QueryReference.test.ts b/src/react/cache/__tests__/QueryReference.test.ts index a35d3138e72..273d074c7e9 100644 --- a/src/react/cache/__tests__/QueryReference.test.ts +++ b/src/react/cache/__tests__/QueryReference.test.ts @@ -18,9 +18,7 @@ test("warns when calling `retain` on a disposed query ref", async () => { using _consoleSpy = spyOnConsole("warn"); const { query, mocks } = useSimpleCase(); const client = createDefaultClient(mocks); - const observable = client.watchQuery>({ - query, - }); + const observable = client.watchQuery({ query }); const queryRef = new InternalQueryReference(observable, {}); const dispose = queryRef.retain(); From 70ce235f850b9fc2bd818cc16f5bebd7fd14bf2a Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 16:45:19 -0700 Subject: [PATCH 112/133] Inline creation of client in test --- .../cache/__tests__/QueryReference.test.ts | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/react/cache/__tests__/QueryReference.test.ts b/src/react/cache/__tests__/QueryReference.test.ts index 273d074c7e9..69309a3ad8e 100644 --- a/src/react/cache/__tests__/QueryReference.test.ts +++ b/src/react/cache/__tests__/QueryReference.test.ts @@ -1,23 +1,15 @@ import { ApolloClient, InMemoryCache } from "../../../core"; -import { MockLink, MockedResponse, wait } from "../../../testing"; -import { - SimpleCaseData, - spyOnConsole, - useSimpleCase, -} from "../../../testing/internal"; +import { MockLink, wait } from "../../../testing"; +import { spyOnConsole, useSimpleCase } from "../../../testing/internal"; import { InternalQueryReference } from "../QueryReference"; -function createDefaultClient(mocks: MockedResponse[]) { - return new ApolloClient({ - cache: new InMemoryCache(), - link: new MockLink(mocks), - }); -} - test("warns when calling `retain` on a disposed query ref", async () => { using _consoleSpy = spyOnConsole("warn"); const { query, mocks } = useSimpleCase(); - const client = createDefaultClient(mocks); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); const observable = client.watchQuery({ query }); const queryRef = new InternalQueryReference(observable, {}); From 74689b4adbc02ad2c9d5661cc50c122cbdc43b1f Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 20:23:13 -0700 Subject: [PATCH 113/133] Add custom matcher to determine if query ref was disposed --- src/testing/matchers/index.d.ts | 6 +++++ src/testing/matchers/index.ts | 2 ++ src/testing/matchers/toBeDisposed.ts | 37 ++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 src/testing/matchers/toBeDisposed.ts diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 690589af128..b73b33cfd08 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -3,6 +3,7 @@ import type { DocumentNode, OperationVariables, } from "../../core/index.js"; +import type { QueryReference } from "../../react/index.js"; import { NextRenderOptions, Profiler, @@ -11,6 +12,11 @@ import { } from "../internal/index.js"; interface ApolloCustomMatchers { + /** + * Used to determine if a queryRef has been disposed. + */ + toBeDisposed: T extends QueryReference ? () => R + : { error: "matcher needs to be called on a QueryReference" }; /** * Used to determine if two GraphQL query documents are equal to each other by * comparing their printed values. The document must be parsed by `gql`. diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index 709bfbad53b..c4f88544f14 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -3,8 +3,10 @@ import { toMatchDocument } from "./toMatchDocument.js"; import { toHaveSuspenseCacheEntryUsing } from "./toHaveSuspenseCacheEntryUsing.js"; import { toRerender, toRenderExactlyTimes } from "./ProfiledComponent.js"; import { toBeGarbageCollected } from "./toBeGarbageCollected.js"; +import { toBeDisposed } from "./toBeDisposed.js"; expect.extend({ + toBeDisposed, toHaveSuspenseCacheEntryUsing, toMatchDocument, toRerender, diff --git a/src/testing/matchers/toBeDisposed.ts b/src/testing/matchers/toBeDisposed.ts new file mode 100644 index 00000000000..e6f47ca59ce --- /dev/null +++ b/src/testing/matchers/toBeDisposed.ts @@ -0,0 +1,37 @@ +import type { MatcherFunction } from "expect"; +import type { QueryReference } from "../../react/cache/QueryReference.js"; +import { + InternalQueryReference, + unwrapQueryRef, +} from "../../react/cache/QueryReference.js"; +import { invariant } from "../../utilities/globals/index.js"; + +function isQueryRef(queryRef: unknown): queryRef is QueryReference { + try { + return unwrapQueryRef(queryRef as any) instanceof InternalQueryReference; + } catch (e) { + return false; + } +} + +export const toBeDisposed: MatcherFunction<[]> = function (queryRef) { + const hint = this.utils.matcherHint("toBeDisposed", "queryRef", "", { + isNot: this.isNot, + }); + + invariant( + isQueryRef(queryRef), + `\n${hint}\n\nmust be called with a valid QueryReference` + ); + + const pass = unwrapQueryRef(queryRef).disposed; + + return { + pass, + message: () => { + return `${hint}\n\nExpected queryRef ${ + this.isNot ? "not " : "" + }to be disposed, but it was${this.isNot ? "" : " not"}.`; + }, + }; +}; From f402d6a71dfc01ab10341100df4952bae9a4815e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 20:26:58 -0700 Subject: [PATCH 114/133] Use toBeDisposed in createQueryPreloader tests --- .../__tests__/createQueryPreloader.test.tsx | 28 ++++++------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index fb265dca975..dfcee09ee04 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -186,9 +186,7 @@ test("Auto disposes of the query ref if not retained within the given time", asy await queryRef.toPromise(); jest.advanceTimersByTime(30_000); - const internalQueryRef = unwrapQueryRef(queryRef); - - expect(internalQueryRef.disposed).toBe(true); + expect(queryRef).toBeDisposed(); expect(client.getObservableQueries().size).toBe(0); expect(client).not.toHaveSuspenseCacheEntryUsing(query); @@ -220,9 +218,7 @@ test("Honors configured auto dispose timer on the client", async () => { await queryRef.toPromise(); jest.advanceTimersByTime(5_000); - const internalQueryRef = unwrapQueryRef(queryRef); - - expect(internalQueryRef.disposed).toBe(true); + expect(queryRef).toBeDisposed(); expect(client.getObservableQueries().size).toBe(0); expect(client).not.toHaveSuspenseCacheEntryUsing(query); @@ -246,9 +242,7 @@ test("useReadQuery auto-retains the queryRef and disposes of it when unmounted", await act(() => queryRef.toPromise()); jest.advanceTimersByTime(30_000); - const internalQueryRef = unwrapQueryRef(queryRef); - - expect(internalQueryRef.disposed).toBe(false); + expect(queryRef).not.toBeDisposed(); jest.useRealTimers(); @@ -256,7 +250,7 @@ test("useReadQuery auto-retains the queryRef and disposes of it when unmounted", await wait(0); - expect(internalQueryRef.disposed).toBe(true); + expect(queryRef).toBeDisposed(); expect(client.getObservableQueries().size).toBe(0); expect(client).not.toHaveSuspenseCacheEntryUsing(query); }); @@ -274,14 +268,12 @@ test("unmounting useReadQuery does not auto dispose of the queryRef when manuall await act(() => queryRef.toPromise()); - const internalQueryRef = unwrapQueryRef(queryRef); - - expect(internalQueryRef.disposed).toBe(false); + expect(queryRef).not.toBeDisposed(); unmount(); await wait(0); - expect(internalQueryRef.disposed).toBe(false); + expect(queryRef).not.toBeDisposed(); dispose(); }); @@ -299,19 +291,17 @@ test("manually disposing of the queryRef after mounting useReadQuery does not di await act(() => queryRef.toPromise()); - const internalQueryRef = unwrapQueryRef(queryRef); - - expect(internalQueryRef.disposed).toBe(false); + expect(queryRef).not.toBeDisposed(); dispose(); await wait(0); - expect(internalQueryRef.disposed).toBe(false); + expect(queryRef).not.toBeDisposed(); unmount(); await wait(0); - expect(internalQueryRef.disposed).toBe(true); + expect(queryRef).toBeDisposed(); }); test("useReadQuery warns when called with a disposed queryRef", async () => { From 0f1c13043aacc72d014fb83314f22cfb2a84966d Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 20:31:37 -0700 Subject: [PATCH 115/133] Move preloadQuery types into exported interface --- src/react/index.ts | 1 + .../query-preloader/createQueryPreloader.ts | 37 +++++++------------ 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/src/react/index.ts b/src/react/index.ts index 4205851640d..13f1103e41f 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -16,6 +16,7 @@ export { DocumentType, operationName, parser } from "./parser/index.js"; export type { PreloadQueryOptions, PreloadQueryFetchPolicy, + PreloadQueryFunction, } from "./query-preloader/createQueryPreloader.js"; export { createQueryPreloader } from "./query-preloader/createQueryPreloader.js"; diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index d0e5c44dfe2..9f5a687d963 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -57,10 +57,8 @@ type PreloadQueryOptionsArg< Omit, ]; -export function createQueryPreloader(client: ApolloClient) { - const suspenseCache = getSuspenseCache(client); - - function preloadQuery< +export interface PreloadQueryFunction { + < TData, TVariables extends OperationVariables, TOptions extends Omit, @@ -77,10 +75,7 @@ export function createQueryPreloader(client: ApolloClient) { TVariables >; - function preloadQuery< - TData = unknown, - TVariables extends OperationVariables = OperationVariables, - >( + ( query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { returnPartialData: true; @@ -88,42 +83,38 @@ export function createQueryPreloader(client: ApolloClient) { } ): QueryReference | undefined, TVariables>; - function preloadQuery< - TData = unknown, - TVariables extends OperationVariables = OperationVariables, - >( + ( query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { errorPolicy: "ignore" | "all"; } ): QueryReference; - function preloadQuery< - TData = unknown, - TVariables extends OperationVariables = OperationVariables, - >( + ( query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { returnPartialData: true; } ): QueryReference, TVariables>; - function preloadQuery< - TData = unknown, - TVariables extends OperationVariables = OperationVariables, - >( + ( query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg> ): QueryReference; +} + +export function createQueryPreloader( + client: ApolloClient +): PreloadQueryFunction { + const suspenseCache = getSuspenseCache(client); function preloadQuery< TData = unknown, TVariables extends OperationVariables = OperationVariables, >( query: DocumentNode | TypedDocumentNode, - options: PreloadQueryOptions & VariablesOption = Object.create( - null - ) + options: PreloadQueryOptions> & + VariablesOption = Object.create(null) ): QueryReference { const { variables, queryKey = [], ...watchQueryOptions } = options; From 10c6db9eb2a7e180384d1d5786d768a4dc421dd0 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 20:38:45 -0700 Subject: [PATCH 116/133] Use invariant from ts-invariant --- src/testing/matchers/toBeDisposed.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/testing/matchers/toBeDisposed.ts b/src/testing/matchers/toBeDisposed.ts index e6f47ca59ce..e613e2698f8 100644 --- a/src/testing/matchers/toBeDisposed.ts +++ b/src/testing/matchers/toBeDisposed.ts @@ -4,7 +4,7 @@ import { InternalQueryReference, unwrapQueryRef, } from "../../react/cache/QueryReference.js"; -import { invariant } from "../../utilities/globals/index.js"; +import { invariant } from "ts-invariant"; function isQueryRef(queryRef: unknown): queryRef is QueryReference { try { From d353057c30b8fe95fede5626b58e200e005a259e Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 20:43:26 -0700 Subject: [PATCH 117/133] Don't use invariant in toBeDisposed --- src/testing/matchers/toBeDisposed.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/testing/matchers/toBeDisposed.ts b/src/testing/matchers/toBeDisposed.ts index e613e2698f8..452cd45ef6b 100644 --- a/src/testing/matchers/toBeDisposed.ts +++ b/src/testing/matchers/toBeDisposed.ts @@ -4,7 +4,6 @@ import { InternalQueryReference, unwrapQueryRef, } from "../../react/cache/QueryReference.js"; -import { invariant } from "ts-invariant"; function isQueryRef(queryRef: unknown): queryRef is QueryReference { try { @@ -19,10 +18,9 @@ export const toBeDisposed: MatcherFunction<[]> = function (queryRef) { isNot: this.isNot, }); - invariant( - isQueryRef(queryRef), - `\n${hint}\n\nmust be called with a valid QueryReference` - ); + if (!isQueryRef(queryRef)) { + throw new Error(`\n${hint}\n\nmust be called with a valid QueryReference`); + } const pass = unwrapQueryRef(queryRef).disposed; From a61e3537537eb707a9e32ccc15e2eede54da42e2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 20:48:59 -0700 Subject: [PATCH 118/133] Import getSuspenseCache from cache/index.js --- src/react/query-preloader/createQueryPreloader.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 9f5a687d963..b76c49e8674 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -16,7 +16,7 @@ import type { } from "../../utilities/index.js"; import { wrapQueryRef } from "../cache/QueryReference.js"; import type { QueryReference } from "../cache/QueryReference.js"; -import { getSuspenseCache } from "../cache/getSuspenseCache.js"; +import { getSuspenseCache } from "../cache/index.js"; import type { CacheKey } from "../cache/types.js"; import type { NoInfer } from "../index.js"; From 51ac881537d69201b2566063f36e1074353b1d44 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 21:04:20 -0700 Subject: [PATCH 119/133] Ensure mocks are used for renderWithMocks helper --- src/testing/internal/renderHelpers.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/testing/internal/renderHelpers.tsx b/src/testing/internal/renderHelpers.tsx index 32ec44c9573..c47a533d09c 100644 --- a/src/testing/internal/renderHelpers.tsx +++ b/src/testing/internal/renderHelpers.tsx @@ -53,7 +53,6 @@ export function renderWithMocks< >( ui: ReactElement, { - mocks, wrapper: Wrapper = React.Fragment, ...renderOptions }: RenderWithMocksOptions From f0462e79d6cc2a37962a0e768e644a7476a323a2 Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 22:37:01 -0700 Subject: [PATCH 120/133] Better changeset --- .changeset/rare-snakes-melt.md | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.changeset/rare-snakes-melt.md b/.changeset/rare-snakes-melt.md index 23b1d7c4838..6757b401a47 100644 --- a/.changeset/rare-snakes-melt.md +++ b/.changeset/rare-snakes-melt.md @@ -2,4 +2,23 @@ "@apollo/client": minor --- -Add a new `createQueryPreloader` function that allows you to preload queries outside of React +Add the ability to start preloading a query outside React to begin fetching as early as possible. Call `createQueryPreloader` to create a `preloadQuery` function which can be called to start fetching a query. This returns a `queryRef` which is passed to `useReadQuery` and suspended until the query is done fetching. + +```tsx +const preloadQuery = createQueryPreloader(client); +const queryRef = preloadQuery(QUERY, { variables, ...otherOptions }); + +function App() { + return { + Loading}> + + + } +} + +function MyQuery() { + const { data } = useReadQuery(queryRef); + + // do something with data +} +``` From 3178fa168082f7d1d72048416f0464b1e5b5f4ef Mon Sep 17 00:00:00 2001 From: Jerel Miller Date: Mon, 11 Dec 2023 23:03:08 -0700 Subject: [PATCH 121/133] Update API report --- .api-reports/api-report-react.md | 113 ++++++++++++++++++++++--- .api-reports/api-report-react_hooks.md | 51 ++++++++--- .api-reports/api-report.md | 113 ++++++++++++++++++++++--- 3 files changed, 238 insertions(+), 39 deletions(-) diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 7acf9ba2c5a..c7ff296332a 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -542,6 +542,9 @@ type ConcastSourcesIterable = Iterable>; export interface Context extends Record { } +// @public (undocumented) +export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -669,6 +672,9 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +type DisposeFn = () => void; + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts @@ -864,13 +870,15 @@ interface IncrementalPayload { // @public (undocumented) class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts - constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); + constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); // (undocumented) applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts // // (undocumented) didChangeOptions(watchQueryOptions: ObservedOptions): boolean; + // (undocumented) + get disposed(): boolean; // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1377,6 +1385,54 @@ interface PendingPromise extends Promise { status: "pending"; } +// @public (undocumented) +export type PreloadQueryFetchPolicy = Extract; + +// @public (undocumented) +export interface PreloadQueryFunction { + // Warning: (ae-forgotten-export) The symbol "PreloadQueryOptionsArg" needs to be exported by the entry point index.d.ts + // + // (undocumented) + >(query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg, TOptions>): QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + // (undocumented) + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + }): QueryReference | undefined, TVariables>; + // (undocumented) + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + errorPolicy: "ignore" | "all"; + }): QueryReference; + // (undocumented) + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + returnPartialData: true; + }): QueryReference, TVariables>; + // (undocumented) + (query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg>): QueryReference; +} + +// Warning: (ae-forgotten-export) The symbol "VariablesOption" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type PreloadQueryOptions = { + canonizeResults?: boolean; + context?: Context; + errorPolicy?: ErrorPolicy; + fetchPolicy?: PreloadQueryFetchPolicy; + queryKey?: string | number | any[]; + returnPartialData?: boolean; + refetchWritePolicy?: RefetchWritePolicy; +} & VariablesOption; + +// @public (undocumented) +type PreloadQueryOptionsArg = [TVariables] extends [never] ? [ +options?: PreloadQueryOptions & TOptions +] : {} extends OnlyRequiredProperties ? [ +options?: PreloadQueryOptions> & Omit +] : [ +options: PreloadQueryOptions> & Omit +]; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -1642,18 +1698,24 @@ interface QueryOptions { // Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "useBackgroundQuery" has more than one declaration; you need to add a TSDoc member reference selector // // @public -export interface QueryReference { +export interface QueryReference { // (undocumented) [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // // (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // Warning: (ae-forgotten-export) The symbol "DisposeFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + retain: () => DisposeFn; + // (undocumented) + toPromise(): Promise>; } // Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @internal (undocumented) type QueryRefPromise = PromiseWithState>; // @public (undocumented) @@ -1998,7 +2060,7 @@ export function useApolloClient(override?: ApolloClient): ApolloClient, "variables">>(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer & TOptions): [ -(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData> | (TOptions["skip"] extends boolean ? undefined : never)), +(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables> | (TOptions["skip"] extends boolean ? undefined : never)), UseBackgroundQueryResult ]; @@ -2007,7 +2069,7 @@ export function useBackgroundQuery | undefined>, +QueryReference | undefined, TVariables>, UseBackgroundQueryResult ]; @@ -2015,7 +2077,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { errorPolicy: "ignore" | "all"; }): [ -QueryReference, +QueryReference, UseBackgroundQueryResult ]; @@ -2024,7 +2086,7 @@ export function useBackgroundQuery> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; @@ -2032,7 +2094,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; }): [ -QueryReference>, +QueryReference, TVariables>, UseBackgroundQueryResult ]; @@ -2040,12 +2102,15 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { skip: boolean; }): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; // @public (undocumented) -export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [QueryReference, UseBackgroundQueryResult]; +export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [ +QueryReference, +UseBackgroundQueryResult +]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken): [undefined, UseBackgroundQueryResult]; @@ -2054,13 +2119,13 @@ export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken | (BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; })): [ -QueryReference> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; @@ -2120,7 +2185,7 @@ export function useLoadableQuery = [ LoadQueryFunction, -QueryReference | null, +QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -2134,6 +2199,17 @@ export function useMutation(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; +// @public (undocumented) +export function useQueryRefHandlers(queryRef: QueryReference): UseQueryRefHandlersResult; + +// @public (undocumented) +export interface UseQueryRefHandlersResult { + // (undocumented) + fetchMore: FetchMoreFunction; + // (undocumented) + refetch: RefetchFunction; +} + // Warning: (ae-forgotten-export) The symbol "ReactiveVar" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -2213,6 +2289,17 @@ export interface UseSuspenseQueryResult; } +// @public (undocumented) +type VariablesOption = [ +TVariables +] extends [never] ? { + variables?: Record; +} : {} extends OnlyRequiredProperties ? { + variables?: TVariables; +} : { + variables: TVariables; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index ec74b11a0be..ac6a6a28a02 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -640,6 +640,9 @@ interface DeleteModifier { // @public (undocumented) const _deleteModifier: unique symbol; +// @public (undocumented) +type DisposeFn = () => void; + // @public (undocumented) class DocumentTransform { // Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts @@ -811,13 +814,15 @@ interface IncrementalPayload { // @public (undocumented) class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts - constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); + constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); // (undocumented) applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts // // (undocumented) didChangeOptions(watchQueryOptions: ObservedOptions): boolean; + // (undocumented) + get disposed(): boolean; // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1560,18 +1565,24 @@ interface QueryOptions { // Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "useBackgroundQuery" has more than one declaration; you need to add a TSDoc member reference selector // // @public -interface QueryReference { +interface QueryReference { // (undocumented) [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // // (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // Warning: (ae-forgotten-export) The symbol "DisposeFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + retain: () => DisposeFn; + // (undocumented) + toPromise(): Promise>; } // Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @internal (undocumented) type QueryRefPromise = PromiseWithState>; // Warning: (ae-forgotten-export) The symbol "ObservableQueryFields" needs to be exported by the entry point index.d.ts @@ -1878,7 +1889,7 @@ export function useApolloClient(override?: ApolloClient): ApolloClient, "variables">>(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer & TOptions): [ -(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData> | (TOptions["skip"] extends boolean ? undefined : never)), +(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables> | (TOptions["skip"] extends boolean ? undefined : never)), UseBackgroundQueryResult ]; @@ -1887,7 +1898,7 @@ export function useBackgroundQuery | undefined>, +QueryReference | undefined, TVariables>, UseBackgroundQueryResult ]; @@ -1895,7 +1906,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { errorPolicy: "ignore" | "all"; }): [ -QueryReference, +QueryReference, UseBackgroundQueryResult ]; @@ -1904,7 +1915,7 @@ export function useBackgroundQuery> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; @@ -1912,7 +1923,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; }): [ -QueryReference>, +QueryReference, TVariables>, UseBackgroundQueryResult ]; @@ -1920,12 +1931,15 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { skip: boolean; }): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; // @public (undocumented) -export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [QueryReference, UseBackgroundQueryResult]; +export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [ +QueryReference, +UseBackgroundQueryResult +]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken): [undefined, UseBackgroundQueryResult]; @@ -1934,13 +1948,13 @@ export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken | (BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; })): [ -QueryReference> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; @@ -2004,7 +2018,7 @@ export function useLoadableQuery = [ LoadQueryFunction, -QueryReference | null, +QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -2021,6 +2035,17 @@ export function useMutation(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; +// @public (undocumented) +export function useQueryRefHandlers(queryRef: QueryReference): UseQueryRefHandlersResult; + +// @public (undocumented) +export interface UseQueryRefHandlersResult { + // (undocumented) + fetchMore: FetchMoreFunction; + // (undocumented) + refetch: RefetchFunction; +} + // Warning: (ae-forgotten-export) The symbol "ReactiveVar" needs to be exported by the entry point index.d.ts // // @public (undocumented) diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 5a22a0eefdd..60afb271e76 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -542,6 +542,9 @@ export const concat: typeof ApolloLink.concat; // @public (undocumented) export const createHttpLink: (linkOptions?: HttpOptions) => ApolloLink; +// @public (undocumented) +export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; + // @public @deprecated (undocumented) export const createSignalIfSupported: () => { controller: boolean; @@ -691,6 +694,9 @@ export { disableExperimentalFragmentVariables } export { disableFragmentWarnings } +// @public (undocumented) +type DisposeFn = () => void; + export { DocumentNode } // @public (undocumented) @@ -1210,13 +1216,15 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { // @public (undocumented) class InternalQueryReference { // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts - constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); + constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); // (undocumented) applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts // // (undocumented) didChangeOptions(watchQueryOptions: ObservedOptions): boolean; + // (undocumented) + get disposed(): boolean; // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions_2" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1905,6 +1913,54 @@ export type PossibleTypesMap = { [supertype: string]: string[]; }; +// @public (undocumented) +export type PreloadQueryFetchPolicy = Extract; + +// @public (undocumented) +export interface PreloadQueryFunction { + // Warning: (ae-forgotten-export) The symbol "PreloadQueryOptionsArg" needs to be exported by the entry point index.d.ts + // + // (undocumented) + >(query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg, TOptions>): QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + // (undocumented) + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + }): QueryReference | undefined, TVariables>; + // (undocumented) + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + errorPolicy: "ignore" | "all"; + }): QueryReference; + // (undocumented) + (query: DocumentNode | TypedDocumentNode, options: PreloadQueryOptions> & { + returnPartialData: true; + }): QueryReference, TVariables>; + // (undocumented) + (query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg>): QueryReference; +} + +// Warning: (ae-forgotten-export) The symbol "VariablesOption" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type PreloadQueryOptions = { + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: PreloadQueryFetchPolicy; + queryKey?: string | number | any[]; + returnPartialData?: boolean; + refetchWritePolicy?: RefetchWritePolicy; +} & VariablesOption; + +// @public (undocumented) +type PreloadQueryOptionsArg = [TVariables] extends [never] ? [ +options?: PreloadQueryOptions & TOptions +] : {} extends OnlyRequiredProperties ? [ +options?: PreloadQueryOptions> & Omit +] : [ +options: PreloadQueryOptions> & Omit +]; + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; @@ -2176,18 +2232,24 @@ export { QueryOptions } // Warning: (ae-unresolved-link) The @link reference could not be resolved: The reference is ambiguous because "useBackgroundQuery" has more than one declaration; you need to add a TSDoc member reference selector // // @public -export interface QueryReference { +export interface QueryReference { // (undocumented) [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // // (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // Warning: (ae-forgotten-export) The symbol "DisposeFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + retain: () => DisposeFn; + // (undocumented) + toPromise(): Promise>; } // Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @internal (undocumented) type QueryRefPromise = PromiseWithState>; // @public (undocumented) @@ -2622,7 +2684,7 @@ export function useApolloClient(override?: ApolloClient): ApolloClient, "variables">>(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer & TOptions): [ -(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData> | (TOptions["skip"] extends boolean ? undefined : never)), +(QueryReference | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables> | (TOptions["skip"] extends boolean ? undefined : never)), UseBackgroundQueryResult ]; @@ -2631,7 +2693,7 @@ export function useBackgroundQuery | undefined>, +QueryReference | undefined, TVariables>, UseBackgroundQueryResult ]; @@ -2639,7 +2701,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { errorPolicy: "ignore" | "all"; }): [ -QueryReference, +QueryReference, UseBackgroundQueryResult ]; @@ -2648,7 +2710,7 @@ export function useBackgroundQuery> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; @@ -2656,7 +2718,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; }): [ -QueryReference>, +QueryReference, TVariables>, UseBackgroundQueryResult ]; @@ -2664,12 +2726,15 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { skip: boolean; }): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; // @public (undocumented) -export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [QueryReference, UseBackgroundQueryResult]; +export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer): [ +QueryReference, +UseBackgroundQueryResult +]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken): [undefined, UseBackgroundQueryResult]; @@ -2678,13 +2743,13 @@ export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: SkipToken | (BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; })): [ -QueryReference> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; // @public (undocumented) export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer): [ -QueryReference | undefined, +QueryReference | undefined, UseBackgroundQueryResult ]; @@ -2744,7 +2809,7 @@ export function useLoadableQuery = [ LoadQueryFunction, -QueryReference | null, +QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -2758,6 +2823,17 @@ export function useMutation(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; +// @public (undocumented) +export function useQueryRefHandlers(queryRef: QueryReference): UseQueryRefHandlersResult; + +// @public (undocumented) +export interface UseQueryRefHandlersResult { + // (undocumented) + fetchMore: FetchMoreFunction; + // (undocumented) + refetch: RefetchFunction; +} + // @public (undocumented) export function useReactiveVar(rv: ReactiveVar): T; @@ -2835,6 +2911,17 @@ export interface UseSuspenseQueryResult; } +// @public (undocumented) +type VariablesOption = [ +TVariables +] extends [never] ? { + variables?: Record; +} : {} extends OnlyRequiredProperties ? { + variables?: TVariables; +} : { + variables: TVariables; +}; + // @public (undocumented) export type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; From a16db666b71534fe0f111d71ccda8d94b2c865eb Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 11:42:41 +0100 Subject: [PATCH 122/133] add `react/internal` entry point, reorganize imports --- config/entryPoints.js | 1 + src/react/cache/index.ts | 2 -- src/react/hooks/useBackgroundQuery.ts | 7 +++---- src/react/hooks/useLoadableQuery.ts | 7 +++---- src/react/hooks/useQueryRefHandlers.ts | 7 ++----- src/react/hooks/useReadQuery.ts | 7 ++----- src/react/hooks/useSuspenseQuery.ts | 4 ++-- src/react/index.ts | 2 +- src/react/{ => internal}/cache/QueryReference.ts | 14 +++++++------- src/react/{ => internal}/cache/SuspenseCache.ts | 4 ++-- .../cache/__tests__/QueryReference.test.ts | 0 src/react/{ => internal}/cache/getSuspenseCache.ts | 7 +++++-- src/react/internal/cache/index.ts | 11 +++++++++++ src/react/{ => internal}/cache/types.ts | 0 src/react/internal/index.ts | 2 ++ src/react/query-preloader/createQueryPreloader.ts | 6 ++---- src/react/types/types.ts | 2 +- src/testing/matchers/toBeDisposed.ts | 4 ++-- .../matchers/toHaveSuspenseCacheEntryUsing.ts | 4 ++-- 19 files changed, 48 insertions(+), 43 deletions(-) delete mode 100644 src/react/cache/index.ts rename src/react/{ => internal}/cache/QueryReference.ts (96%) rename src/react/{ => internal}/cache/SuspenseCache.ts (92%) rename src/react/{ => internal}/cache/__tests__/QueryReference.test.ts (100%) rename src/react/{ => internal}/cache/getSuspenseCache.ts (81%) create mode 100644 src/react/internal/cache/index.ts rename src/react/{ => internal}/cache/types.ts (100%) create mode 100644 src/react/internal/index.ts diff --git a/config/entryPoints.js b/config/entryPoints.js index 3cd167c045e..7c3d71324b5 100644 --- a/config/entryPoints.js +++ b/config/entryPoints.js @@ -18,6 +18,7 @@ const entryPoints = [ { dirs: ["link", "utils"] }, { dirs: ["link", "ws"] }, { dirs: ["react"] }, + { dirs: ["react", "internal"] }, { dirs: ["react", "components"] }, { dirs: ["react", "context"] }, { dirs: ["react", "hoc"] }, diff --git a/src/react/cache/index.ts b/src/react/cache/index.ts deleted file mode 100644 index 25a6d03bee5..00000000000 --- a/src/react/cache/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { SuspenseCacheOptions } from "./SuspenseCache.js"; -export { getSuspenseCache } from "./getSuspenseCache.js"; diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 96ae008360e..86e12e561ad 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -11,16 +11,15 @@ import { unwrapQueryRef, updateWrappedQueryRef, wrapQueryRef, -} from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; + getSuspenseCache, +} from "../internal/index.js"; +import type { QueryReference, CacheKey } from "../internal/index.js"; import type { BackgroundQueryHookOptions, NoInfer } from "../types/types.js"; import { __use } from "./internal/index.js"; -import { getSuspenseCache } from "../cache/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; import type { DeepPartial } from "../../utilities/index.js"; -import type { CacheKey } from "../cache/types.js"; import type { SkipToken } from "./constants.js"; export type UseBackgroundQueryResult< diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index 96f8cc974fa..344b5b5802b 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -11,11 +11,11 @@ import { unwrapQueryRef, updateWrappedQueryRef, wrapQueryRef, -} from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; + getSuspenseCache, +} from "../internal/index.js"; +import type { QueryReference, CacheKey } from "../internal/index.js"; import type { LoadableQueryHookOptions } from "../types/types.js"; import { __use, useRenderGuard } from "./internal/index.js"; -import { getSuspenseCache } from "../cache/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; @@ -23,7 +23,6 @@ import type { DeepPartial, OnlyRequiredProperties, } from "../../utilities/index.js"; -import type { CacheKey } from "../cache/types.js"; import { invariant } from "../../utilities/globals/index.js"; export type LoadQueryFunction = ( diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts index f99e8268e4b..1a2443af362 100644 --- a/src/react/hooks/useQueryRefHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -3,11 +3,8 @@ import { unwrapQueryRef, updateWrappedQueryRef, wrapQueryRef, -} from "../cache/QueryReference.js"; -import type { - QueryRefPromise, - QueryReference, -} from "../cache/QueryReference.js"; +} from "../internal/index.js"; +import type { QueryRefPromise, QueryReference } from "../internal/index.js"; import type { OperationVariables } from "../../core/types.js"; import type { RefetchFunction, FetchMoreFunction } from "./useSuspenseQuery.js"; import type { FetchMoreQueryOptions } from "../../core/watchQueryOptions.js"; diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 075ce684ee7..cc233d860bf 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -1,9 +1,6 @@ import * as React from "rehackt"; -import { - unwrapQueryRef, - updateWrappedQueryRef, -} from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; +import { unwrapQueryRef, updateWrappedQueryRef } from "../internal/index.js"; +import type { QueryReference } from "../internal/index.js"; import { __use } from "./internal/index.js"; import { toApolloError } from "./useSuspenseQuery.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index 9291aade216..3a69e175ed5 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -21,11 +21,11 @@ import type { NoInfer, } from "../types/types.js"; import { __use, useDeepMemo } from "./internal/index.js"; -import { getSuspenseCache } from "../cache/index.js"; +import { getSuspenseCache } from "../internal/index.js"; import { canonicalStringify } from "../../cache/index.js"; import { skipToken } from "./constants.js"; import type { SkipToken } from "./constants.js"; -import type { CacheKey, QueryKey } from "../cache/types.js"; +import type { CacheKey, QueryKey } from "../internal/index.js"; export interface UseSuspenseQueryResult< TData = unknown, diff --git a/src/react/index.ts b/src/react/index.ts index 13f1103e41f..16fd78623a2 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -20,4 +20,4 @@ export type { } from "./query-preloader/createQueryPreloader.js"; export { createQueryPreloader } from "./query-preloader/createQueryPreloader.js"; -export * from "./types/types.js"; +export type * from "./types/types.js"; diff --git a/src/react/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts similarity index 96% rename from src/react/cache/QueryReference.ts rename to src/react/internal/cache/QueryReference.ts index 0ac99708b5c..5243080f3df 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -5,20 +5,20 @@ import type { ObservableQuery, OperationVariables, WatchQueryOptions, -} from "../../core/index.js"; -import { isNetworkRequestSettled } from "../../core/index.js"; +} from "../../../core/index.js"; +import { isNetworkRequestSettled } from "../../../core/index.js"; import type { ObservableSubscription, PromiseWithState, -} from "../../utilities/index.js"; +} from "../../../utilities/index.js"; import { createFulfilledPromise, createRejectedPromise, -} from "../../utilities/index.js"; +} from "../../../utilities/index.js"; import type { QueryKey } from "./types.js"; -import type { useBackgroundQuery, useReadQuery } from "../hooks/index.js"; -import { wrapPromiseWithState } from "../../utilities/index.js"; -import { invariant } from "../../utilities/globals/index.js"; +import type { useBackgroundQuery, useReadQuery } from "../../hooks/index.js"; +import { wrapPromiseWithState } from "../../../utilities/index.js"; +import { invariant } from "../../../utilities/globals/index.js"; /** @internal */ export type QueryRefPromise = PromiseWithState>; diff --git a/src/react/cache/SuspenseCache.ts b/src/react/internal/cache/SuspenseCache.ts similarity index 92% rename from src/react/cache/SuspenseCache.ts rename to src/react/internal/cache/SuspenseCache.ts index 36641c76cb4..d66eb905a5a 100644 --- a/src/react/cache/SuspenseCache.ts +++ b/src/react/internal/cache/SuspenseCache.ts @@ -1,6 +1,6 @@ import { Trie } from "@wry/trie"; -import type { ObservableQuery } from "../../core/index.js"; -import { canUseWeakMap } from "../../utilities/index.js"; +import type { ObservableQuery } from "../../../core/index.js"; +import { canUseWeakMap } from "../../../utilities/index.js"; import { InternalQueryReference } from "./QueryReference.js"; import type { CacheKey } from "./types.js"; diff --git a/src/react/cache/__tests__/QueryReference.test.ts b/src/react/internal/cache/__tests__/QueryReference.test.ts similarity index 100% rename from src/react/cache/__tests__/QueryReference.test.ts rename to src/react/internal/cache/__tests__/QueryReference.test.ts diff --git a/src/react/cache/getSuspenseCache.ts b/src/react/internal/cache/getSuspenseCache.ts similarity index 81% rename from src/react/cache/getSuspenseCache.ts rename to src/react/internal/cache/getSuspenseCache.ts index 93fbc72b98a..8ba7aaebafb 100644 --- a/src/react/cache/getSuspenseCache.ts +++ b/src/react/internal/cache/getSuspenseCache.ts @@ -1,8 +1,8 @@ import type { SuspenseCacheOptions } from "./index.js"; import { SuspenseCache } from "./SuspenseCache.js"; -import type { ApolloClient } from "../../core/ApolloClient.js"; +import type { ApolloClient } from "../../../core/ApolloClient.js"; -declare module "../../core/ApolloClient.js" { +declare module "../../../core/ApolloClient.js" { interface DefaultOptions { react?: { suspense?: Readonly; @@ -12,6 +12,9 @@ declare module "../../core/ApolloClient.js" { const suspenseCacheSymbol = Symbol.for("apollo.suspenseCache"); +/** + * @internal + */ export function getSuspenseCache( client: ApolloClient & { [suspenseCacheSymbol]?: SuspenseCache; diff --git a/src/react/internal/cache/index.ts b/src/react/internal/cache/index.ts new file mode 100644 index 00000000000..b545f1cdffa --- /dev/null +++ b/src/react/internal/cache/index.ts @@ -0,0 +1,11 @@ +export type { SuspenseCacheOptions } from "./SuspenseCache.js"; +export type { CacheKey, QueryKey } from "./types.js"; +export { getSuspenseCache } from "./getSuspenseCache.js"; +export type { QueryReference, QueryRefPromise } from "./QueryReference.js"; +export { + InternalQueryReference, + unwrapQueryRef, + wrapQueryRef, + updateWrappedQueryRef, + getWrappedPromise, +} from "./QueryReference.js"; diff --git a/src/react/cache/types.ts b/src/react/internal/cache/types.ts similarity index 100% rename from src/react/cache/types.ts rename to src/react/internal/cache/types.ts diff --git a/src/react/internal/index.ts b/src/react/internal/index.ts new file mode 100644 index 00000000000..9ae0a3b1f65 --- /dev/null +++ b/src/react/internal/index.ts @@ -0,0 +1,2 @@ +export type * from "./cache/index.js"; +export * from "./cache/index.js"; diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index b76c49e8674..30fa013bebb 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -14,10 +14,8 @@ import type { DeepPartial, OnlyRequiredProperties, } from "../../utilities/index.js"; -import { wrapQueryRef } from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; -import { getSuspenseCache } from "../cache/index.js"; -import type { CacheKey } from "../cache/types.js"; +import { getSuspenseCache, wrapQueryRef } from "../internal/cache/index.js"; +import type { CacheKey, QueryReference } from "../internal/cache/index.js"; import type { NoInfer } from "../index.js"; type VariablesOption = diff --git a/src/react/types/types.ts b/src/react/types/types.ts index f6f7af613aa..a02718548ff 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -26,7 +26,7 @@ import type { /* QueryReference type */ -export type { QueryReference } from "../cache/QueryReference.js"; +export type { QueryReference } from "../internal/cache/QueryReference.js"; /* Common types */ diff --git a/src/testing/matchers/toBeDisposed.ts b/src/testing/matchers/toBeDisposed.ts index 452cd45ef6b..beab7c05528 100644 --- a/src/testing/matchers/toBeDisposed.ts +++ b/src/testing/matchers/toBeDisposed.ts @@ -1,9 +1,9 @@ import type { MatcherFunction } from "expect"; -import type { QueryReference } from "../../react/cache/QueryReference.js"; +import type { QueryReference } from "../../react/index.js"; import { InternalQueryReference, unwrapQueryRef, -} from "../../react/cache/QueryReference.js"; +} from "../../react/internal/index.js"; function isQueryRef(queryRef: unknown): queryRef is QueryReference { try { diff --git a/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts b/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts index 7244952fed6..9cfdd2c1d78 100644 --- a/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts +++ b/src/testing/matchers/toHaveSuspenseCacheEntryUsing.ts @@ -3,8 +3,8 @@ import type { DocumentNode } from "graphql"; import type { OperationVariables } from "../../core/index.js"; import { ApolloClient } from "../../core/index.js"; import { canonicalStringify } from "../../cache/index.js"; -import { getSuspenseCache } from "../../react/cache/index.js"; -import type { CacheKey } from "../../react/cache/types.js"; +import { getSuspenseCache } from "../../react/internal/index.js"; +import type { CacheKey } from "../../react/internal/index.js"; export const toHaveSuspenseCacheEntryUsing: MatcherFunction< [ From c526c7aec837859bd764cd86f0da03a7352825d3 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 11:46:51 +0100 Subject: [PATCH 123/133] fix up tests --- src/react/hooks/__tests__/useBackgroundQuery.test.tsx | 2 +- src/react/hooks/__tests__/useQueryRefHandlers.test.tsx | 2 +- src/react/internal/cache/__tests__/QueryReference.test.ts | 6 +++--- .../query-preloader/__tests__/createQueryPreloader.test.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index e02e0f77f57..dfaa85c6b13 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -44,7 +44,7 @@ import { import { useBackgroundQuery } from "../useBackgroundQuery"; import { useReadQuery } from "../useReadQuery"; import { ApolloProvider } from "../../context"; -import { QueryReference } from "../../cache/QueryReference"; +import { QueryReference } from "../../internal/index.js"; import { InMemoryCache } from "../../../cache"; import { SuspenseQueryHookFetchPolicy, diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index 491bb9cacc9..5ce723afa4c 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -22,7 +22,7 @@ import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; import { Suspense } from "react"; import { createQueryPreloader } from "../../query-preloader/createQueryPreloader"; import userEvent from "@testing-library/user-event"; -import { QueryReference } from "../../cache/QueryReference"; +import { QueryReference } from "../../internal/index.js"; import { useBackgroundQuery } from "../useBackgroundQuery"; import { useLoadableQuery } from "../useLoadableQuery"; import { concatPagination } from "../../../utilities"; diff --git a/src/react/internal/cache/__tests__/QueryReference.test.ts b/src/react/internal/cache/__tests__/QueryReference.test.ts index 69309a3ad8e..ed55902a3b9 100644 --- a/src/react/internal/cache/__tests__/QueryReference.test.ts +++ b/src/react/internal/cache/__tests__/QueryReference.test.ts @@ -1,6 +1,6 @@ -import { ApolloClient, InMemoryCache } from "../../../core"; -import { MockLink, wait } from "../../../testing"; -import { spyOnConsole, useSimpleCase } from "../../../testing/internal"; +import { ApolloClient, InMemoryCache } from "../../../../core"; +import { MockLink, wait } from "../../../../testing"; +import { spyOnConsole, useSimpleCase } from "../../../../testing/internal"; import { InternalQueryReference } from "../QueryReference"; test("warns when calling `retain` on a disposed query ref", async () => { diff --git a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx index dfcee09ee04..e9e1c2350c6 100644 --- a/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -16,7 +16,7 @@ import { wait, } from "../../../testing"; import { expectTypeOf } from "expect-type"; -import { QueryReference, unwrapQueryRef } from "../../cache/QueryReference"; +import { QueryReference, unwrapQueryRef } from "../../internal/index.js"; import { DeepPartial, Observable } from "../../../utilities"; import { SimpleCaseData, From a2d17de31cb0c639a4fa3a7cc64a81ff205f5746 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 11:56:29 +0100 Subject: [PATCH 124/133] execute "Compare Build Output" for all PRs --- .github/workflows/compare-build-output.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/compare-build-output.yml b/.github/workflows/compare-build-output.yml index dedf208c41e..fcfd5503bdf 100644 --- a/.github/workflows/compare-build-output.yml +++ b/.github/workflows/compare-build-output.yml @@ -1,9 +1,5 @@ name: Compare Build Output -on: - pull_request: - branches: - - main - - release-* +on: pull_request concurrency: ${{ github.workflow }}-${{ github.ref }} From dca7a914e26794b29d8882804a200eba1fdbeacb Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 12:59:33 +0100 Subject: [PATCH 125/133] fix up some imports --- src/react/query-preloader/createQueryPreloader.ts | 4 ++-- src/react/types/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 30fa013bebb..0f0b3a0d40d 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -14,8 +14,8 @@ import type { DeepPartial, OnlyRequiredProperties, } from "../../utilities/index.js"; -import { getSuspenseCache, wrapQueryRef } from "../internal/cache/index.js"; -import type { CacheKey, QueryReference } from "../internal/cache/index.js"; +import { getSuspenseCache, wrapQueryRef } from "../internal/index.js"; +import type { CacheKey, QueryReference } from "../internal/index.js"; import type { NoInfer } from "../index.js"; type VariablesOption = diff --git a/src/react/types/types.ts b/src/react/types/types.ts index a02718548ff..2714d6f1077 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -26,7 +26,7 @@ import type { /* QueryReference type */ -export type { QueryReference } from "../internal/cache/QueryReference.js"; +export type { QueryReference } from "../internal/index.js"; /* Common types */ From e72d3c5f37afb93351ce0fa3842f94c5b8370950 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 12:59:43 +0100 Subject: [PATCH 126/133] change re-export to type export --- src/core/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/index.ts b/src/core/index.ts index 5757cdb2071..43036f169b8 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -50,7 +50,7 @@ export { makeVar, } from "../cache/index.js"; -export * from "../cache/inmemory/types.js"; +export type * from "../cache/inmemory/types.js"; /* Link */ From fae470d3e6ebf2f5fdcf52bd882ca93b8c531e5c Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 13:00:48 +0100 Subject: [PATCH 127/133] chores --- .api-reports/api-report-react.md | 6 +- .api-reports/api-report-react_hooks.md | 6 +- .api-reports/api-report-react_internal.md | 1639 +++++++++++++++++++++ .api-reports/api-report.md | 6 +- 4 files changed, 1648 insertions(+), 9 deletions(-) create mode 100644 .api-reports/api-report-react_internal.md diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index c7ff296332a..674e60b090c 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -2345,9 +2345,9 @@ interface WatchQueryOptions Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { ASTNode } from 'graphql'; +import type { DocumentNode } from 'graphql'; +import type { ExecutionResult } from 'graphql'; +import type { FieldNode } from 'graphql'; +import type { FragmentDefinitionNode } from 'graphql'; +import type { GraphQLError } from 'graphql'; +import type { GraphQLErrorExtensions } from 'graphql'; +import { Observable } from 'zen-observable-ts'; +import type { Observer } from 'zen-observable-ts'; +import type { Subscriber } from 'zen-observable-ts'; +import type { Subscription } from 'zen-observable-ts'; +import { Trie } from '@wry/trie'; +import { TypedDocumentNode } from '@graphql-typed-document-node/core'; + +// Warning: (ae-forgotten-export) The symbol "Modifier" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "StoreObjectValueMaybeReference" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type AllFieldsModifier> = Modifier> : never>; + +// Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +abstract class ApolloCache implements DataProxy { + // (undocumented) + readonly assumeImmutableResults: boolean; + // (undocumented) + batch(options: Cache_2.BatchOptions): U; + // (undocumented) + abstract diff(query: Cache_2.DiffOptions): Cache_2.DiffResult; + // (undocumented) + abstract evict(options: Cache_2.EvictOptions): boolean; + abstract extract(optimistic?: boolean): TSerialized; + // (undocumented) + gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts + // + // (undocumented) + identify(object: StoreObject | Reference): string | undefined; + // (undocumented) + modify = Record>(options: Cache_2.ModifyOptions): boolean; + // Warning: (ae-forgotten-export) The symbol "Transaction" needs to be exported by the entry point index.d.ts + // + // (undocumented) + abstract performTransaction(transaction: Transaction, optimisticId?: string | null): void; + // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + abstract read(query: Cache_2.ReadOptions): TData | null; + // (undocumented) + readFragment(options: Cache_2.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + // (undocumented) + readQuery(options: Cache_2.ReadQueryOptions, optimistic?: boolean): QueryType | null; + // (undocumented) + recordOptimisticTransaction(transaction: Transaction, optimisticId: string): void; + // (undocumented) + abstract removeOptimistic(id: string): void; + // (undocumented) + abstract reset(options?: Cache_2.ResetOptions): Promise; + abstract restore(serializedState: TSerialized): ApolloCache; + // (undocumented) + transformDocument(document: DocumentNode): DocumentNode; + // (undocumented) + transformForLink(document: DocumentNode): DocumentNode; + // (undocumented) + updateFragment(options: Cache_2.UpdateFragmentOptions, update: (data: TData | null) => TData | null | void): TData | null; + // (undocumented) + updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; + // (undocumented) + abstract watch(watch: Cache_2.WatchOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts + // + // (undocumented) + abstract write(write: Cache_2.WriteOptions): Reference | undefined; + // (undocumented) + writeFragment({ id, data, fragment, fragmentName, ...options }: Cache_2.WriteFragmentOptions): Reference | undefined; + // (undocumented) + writeQuery({ id, data, ...options }: Cache_2.WriteQueryOptions): Reference | undefined; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "Observable" +// +// @public +class ApolloClient implements DataProxy { + // (undocumented) + __actionHookForDevTools(cb: () => any): void; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + constructor(options: ApolloClientOptions); + // Warning: (ae-forgotten-export) The symbol "GraphQLRequest" needs to be exported by the entry point index.d.ts + // + // (undocumented) + __requestRaw(payload: GraphQLRequest): Observable; + // Warning: (ae-forgotten-export) The symbol "Resolvers" needs to be exported by the entry point index.d.ts + addResolvers(resolvers: Resolvers | Resolvers[]): void; + // Warning: (ae-forgotten-export) The symbol "ApolloCache" needs to be exported by the entry point index.d.ts + // + // (undocumented) + cache: ApolloCache; + clearStore(): Promise; + // (undocumented) + get defaultContext(): Partial; + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + disableNetworkFetches: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransform" needs to be exported by the entry point index.d.ts + get documentTransform(): DocumentTransform; + extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts + getObservableQueries(include?: RefetchQueriesInclude): Map>; + getResolvers(): Resolvers; + // Warning: (ae-forgotten-export) The symbol "ApolloLink" needs to be exported by the entry point index.d.ts + // + // (undocumented) + link: ApolloLink; + // Warning: (ae-forgotten-export) The symbol "DefaultContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "MutationOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "FetchResult" needs to be exported by the entry point index.d.ts + mutate = DefaultContext, TCache extends ApolloCache = ApolloCache>(options: MutationOptions): Promise>; + onClearStore(cb: () => Promise): () => void; + onResetStore(cb: () => Promise): () => void; + // Warning: (ae-forgotten-export) The symbol "QueryOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ApolloQueryResult" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "QueryOptions" + query(options: QueryOptions): Promise>; + // (undocumented) + queryDeduplication: boolean; + readFragment(options: DataProxy.Fragment, optimistic?: boolean): T | null; + readQuery(options: DataProxy.Query, optimistic?: boolean): T | null; + reFetchObservableQueries(includeStandby?: boolean): Promise[]>; + // Warning: (ae-forgotten-export) The symbol "RefetchQueriesOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "RefetchQueriesResult" needs to be exported by the entry point index.d.ts + refetchQueries = ApolloCache, TResult = Promise>>(options: RefetchQueriesOptions): RefetchQueriesResult; + resetStore(): Promise[] | null>; + restore(serializedState: TCacheShape): ApolloCache; + setLink(newLink: ApolloLink): void; + // Warning: (ae-forgotten-export) The symbol "FragmentMatcher" needs to be exported by the entry point index.d.ts + setLocalStateFragmentMatcher(fragmentMatcher: FragmentMatcher): void; + setResolvers(resolvers: Resolvers | Resolvers[]): void; + stop(): void; + // Warning: (ae-forgotten-export) The symbol "SubscriptionOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "Observable" + subscribe(options: SubscriptionOptions): Observable>; + // Warning: (ae-forgotten-export) The symbol "ApolloClientOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly typeDefs: ApolloClientOptions["typeDefs"]; + // (undocumented) + version: string; + // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchQueryOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ObservableQuery" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ObservableQuery" + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ObservableQuery" + watchQuery(options: WatchQueryOptions): ObservableQuery; + writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; + writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; +} + +// @public (undocumented) +interface ApolloClientOptions { + assumeImmutableResults?: boolean; + cache: ApolloCache; + connectToDevTools?: boolean; + // (undocumented) + credentials?: string; + // (undocumented) + defaultContext?: Partial; + defaultOptions?: DefaultOptions; + // (undocumented) + documentTransform?: DocumentTransform; + // (undocumented) + fragmentMatcher?: FragmentMatcher; + // (undocumented) + headers?: Record; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloLink" + link?: ApolloLink; + name?: string; + queryDeduplication?: boolean; + // (undocumented) + resolvers?: Resolvers | Resolvers[]; + ssrForceFetchDelay?: number; + ssrMode?: boolean; + // (undocumented) + typeDefs?: string | string[] | DocumentNode | DocumentNode[]; + // Warning: (ae-forgotten-export) The symbol "UriFunction" needs to be exported by the entry point index.d.ts + uri?: string | UriFunction; + version?: string; +} + +// @public (undocumented) +class ApolloError extends Error { + // Warning: (ae-forgotten-export) The symbol "ApolloErrorOptions" needs to be exported by the entry point index.d.ts + constructor({ graphQLErrors, protocolErrors, clientErrors, networkError, errorMessage, extraInfo, }: ApolloErrorOptions); + // (undocumented) + clientErrors: ReadonlyArray; + // (undocumented) + extraInfo: any; + // Warning: (ae-forgotten-export) The symbol "GraphQLErrors" needs to be exported by the entry point index.d.ts + // + // (undocumented) + graphQLErrors: GraphQLErrors; + // (undocumented) + message: string; + // (undocumented) + name: string; + // Warning: (ae-forgotten-export) The symbol "ServerParseError" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ServerError" needs to be exported by the entry point index.d.ts + // + // (undocumented) + networkError: Error | ServerParseError | ServerError | null; + // (undocumented) + protocolErrors: ReadonlyArray<{ + message: string; + extensions?: GraphQLErrorExtensions[]; + }>; +} + +// @public (undocumented) +interface ApolloErrorOptions { + // (undocumented) + clientErrors?: ReadonlyArray; + // (undocumented) + errorMessage?: string; + // (undocumented) + extraInfo?: any; + // (undocumented) + graphQLErrors?: ReadonlyArray; + // (undocumented) + networkError?: Error | ServerParseError | ServerError | null; + // (undocumented) + protocolErrors?: ReadonlyArray<{ + message: string; + extensions?: GraphQLErrorExtensions[]; + }>; +} + +// @public (undocumented) +class ApolloLink { + constructor(request?: RequestHandler); + // (undocumented) + static concat(first: ApolloLink | RequestHandler, second: ApolloLink | RequestHandler): ApolloLink; + // (undocumented) + concat(next: ApolloLink | RequestHandler): ApolloLink; + // (undocumented) + static empty(): ApolloLink; + // (undocumented) + static execute(link: ApolloLink, operation: GraphQLRequest): Observable; + // Warning: (ae-forgotten-export) The symbol "RequestHandler" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // (undocumented) + protected onError(error: any, observer?: Observer): false | void; + // Warning: (ae-forgotten-export) The symbol "NextLink" needs to be exported by the entry point index.d.ts + // + // (undocumented) + request(operation: Operation, forward?: NextLink): Observable | null; + // (undocumented) + setOnError(fn: ApolloLink["onError"]): this; + // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts + // + // (undocumented) + static split(test: (op: Operation) => boolean, left: ApolloLink | RequestHandler, right?: ApolloLink | RequestHandler): ApolloLink; + // (undocumented) + split(test: (op: Operation) => boolean, left: ApolloLink | RequestHandler, right?: ApolloLink | RequestHandler): ApolloLink; +} + +// @public (undocumented) +type ApolloQueryResult = { + data: T; + errors?: ReadonlyArray; + error?: ApolloError; + loading: boolean; + networkStatus: NetworkStatus; + partial?: boolean; +}; + +// @public +type AsStoreObject = { + [K in keyof T]: T[K]; +}; + +// @public (undocumented) +namespace Cache_2 { + // (undocumented) + interface BatchOptions, TUpdateResult = void> { + // (undocumented) + onWatchUpdated?: (this: TCache, watch: Cache_2.WatchOptions, diff: Cache_2.DiffResult, lastDiff?: Cache_2.DiffResult | undefined) => any; + // (undocumented) + optimistic?: string | boolean; + // (undocumented) + removeOptimistic?: string; + // (undocumented) + update(cache: TCache): TUpdateResult; + } + // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface DiffOptions extends Omit, "rootId"> { + } + // (undocumented) + interface EvictOptions { + // (undocumented) + args?: Record; + // (undocumented) + broadcast?: boolean; + // (undocumented) + fieldName?: string; + // (undocumented) + id?: string; + } + // (undocumented) + interface ModifyOptions = Record> { + // (undocumented) + broadcast?: boolean; + // Warning: (ae-forgotten-export) The symbol "Modifiers" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "AllFieldsModifier" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fields: Modifiers | AllFieldsModifier; + // (undocumented) + id?: string; + // (undocumented) + optimistic?: boolean; + } + // (undocumented) + interface ReadOptions extends DataProxy.Query { + // (undocumented) + canonizeResults?: boolean; + // (undocumented) + optimistic: boolean; + // (undocumented) + previousResult?: any; + // (undocumented) + returnPartialData?: boolean; + // (undocumented) + rootId?: string; + } + // (undocumented) + interface ResetOptions { + // (undocumented) + discardWatches?: boolean; + } + // (undocumented) + type WatchCallback = (diff: Cache_2.DiffResult, lastDiff?: Cache_2.DiffResult) => void; + // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface WatchOptions extends DiffOptions { + // Warning: (ae-forgotten-export) The symbol "Cache_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + callback: WatchCallback; + // (undocumented) + immediate?: boolean; + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + lastDiff?: DiffResult; + // (undocumented) + watcher?: object; + } + // (undocumented) + interface WriteOptions extends Omit, "id">, Omit, "data"> { + // (undocumented) + dataId?: string; + // (undocumented) + result: TResult; + } + import DiffResult = DataProxy.DiffResult; + import ReadQueryOptions = DataProxy.ReadQueryOptions; + import ReadFragmentOptions = DataProxy.ReadFragmentOptions; + import WriteQueryOptions = DataProxy.WriteQueryOptions; + import WriteFragmentOptions = DataProxy.WriteFragmentOptions; + import UpdateQueryOptions = DataProxy.UpdateQueryOptions; + import UpdateFragmentOptions = DataProxy.UpdateFragmentOptions; + import Fragment = DataProxy.Fragment; +} + +// @public (undocumented) +export type CacheKey = [ +query: DocumentNode, +stringifiedVariables: string, +...queryKey: any[] +]; + +// @public (undocumented) +const enum CacheWriteBehavior { + // (undocumented) + FORBID = 0, + // (undocumented) + MERGE = 2, + // (undocumented) + OVERWRITE = 1 +} + +// Warning: (ae-forgotten-export) The symbol "StoreValue" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type CanReadFunction = (value: StoreValue) => boolean; + +// @public (undocumented) +class Concast extends Observable { + // Warning: (ae-forgotten-export) The symbol "MaybeAsync" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "ConcastSourcesIterable" needs to be exported by the entry point index.d.ts + constructor(sources: MaybeAsync> | Subscriber); + // (undocumented) + addObserver(observer: Observer): void; + // Warning: (ae-forgotten-export) The symbol "NextResultListener" needs to be exported by the entry point index.d.ts + // + // (undocumented) + beforeNext(callback: NextResultListener): void; + // (undocumented) + cancel: (reason: any) => void; + // (undocumented) + readonly promise: Promise; + // (undocumented) + removeObserver(observer: Observer): void; +} + +// Warning: (ae-forgotten-export) The symbol "Source" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type ConcastSourcesIterable = Iterable>; + +// @public (undocumented) +namespace DataProxy { + // (undocumented) + type DiffResult = { + result?: T; + complete?: boolean; + missing?: MissingFieldError[]; + fromOptimisticTransaction?: boolean; + }; + // (undocumented) + interface Fragment { + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + id?: string; + variables?: TVariables; + } + // (undocumented) + interface Query { + id?: string; + query: DocumentNode | TypedDocumentNode; + variables?: TVariables; + } + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface ReadFragmentOptions extends Fragment { + canonizeResults?: boolean; + optimistic?: boolean; + returnPartialData?: boolean; + } + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface ReadQueryOptions extends Query { + canonizeResults?: boolean; + optimistic?: boolean; + returnPartialData?: boolean; + } + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface UpdateFragmentOptions extends Omit & WriteFragmentOptions, "data"> { + } + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface UpdateQueryOptions extends Omit & WriteQueryOptions, "data"> { + } + // (undocumented) + interface WriteFragmentOptions extends Fragment, WriteOptions { + } + // (undocumented) + interface WriteOptions { + broadcast?: boolean; + data: TData; + overwrite?: boolean; + } + // Warning: (ae-forgotten-export) The symbol "DataProxy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + interface WriteQueryOptions extends Query, WriteOptions { + } +} + +// @public +interface DataProxy { + readFragment(options: DataProxy.ReadFragmentOptions, optimistic?: boolean): FragmentType | null; + readQuery(options: DataProxy.ReadQueryOptions, optimistic?: boolean): QueryType | null; + writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; + writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; +} + +// @public (undocumented) +interface DefaultContext extends Record { +} + +// @public (undocumented) +interface DefaultOptions { + // (undocumented) + mutate?: Partial>; + // (undocumented) + query?: Partial>; + // (undocumented) + watchQuery?: Partial>; +} + +// @public (undocumented) +interface DeleteModifier { + // (undocumented) + [_deleteModifier]: true; +} + +// @public (undocumented) +const _deleteModifier: unique symbol; + +// @public (undocumented) +type DisposeFn = () => void; + +// @public (undocumented) +class DocumentTransform { + // Warning: (ae-forgotten-export) The symbol "TransformFn" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "DocumentTransformOptions" needs to be exported by the entry point index.d.ts + constructor(transform: TransformFn, options?: DocumentTransformOptions); + // (undocumented) + concat(otherTransform: DocumentTransform): DocumentTransform; + // (undocumented) + static identity(): DocumentTransform; + resetCache(): void; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // (undocumented) + transformDocument(document: DocumentNode): DocumentNode; +} + +// @public (undocumented) +type DocumentTransformCacheKey = ReadonlyArray; + +// @public (undocumented) +interface DocumentTransformOptions { + // (undocumented) + cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; +} + +// @public +type ErrorPolicy = "none" | "ignore" | "all"; + +// Warning: (ae-forgotten-export) The symbol "ExecutionPatchResultBase" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface ExecutionPatchIncrementalResult, TExtensions = Record> extends ExecutionPatchResultBase { + // (undocumented) + data?: never; + // (undocumented) + errors?: never; + // (undocumented) + extensions?: never; + // Warning: (ae-forgotten-export) The symbol "IncrementalPayload" needs to be exported by the entry point index.d.ts + // + // (undocumented) + incremental?: IncrementalPayload[]; +} + +// @public (undocumented) +interface ExecutionPatchInitialResult, TExtensions = Record> extends ExecutionPatchResultBase { + // (undocumented) + data: TData | null | undefined; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; + // (undocumented) + incremental?: never; +} + +// Warning: (ae-forgotten-export) The symbol "ExecutionPatchInitialResult" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionPatchIncrementalResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type ExecutionPatchResult, TExtensions = Record> = ExecutionPatchInitialResult | ExecutionPatchIncrementalResult; + +// @public (undocumented) +interface ExecutionPatchResultBase { + // (undocumented) + hasNext?: boolean; +} + +// @public (undocumented) +type FetchMoreOptions = Parameters["fetchMore"]>[0]; + +// @public (undocumented) +interface FetchMoreQueryOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) + query?: DocumentNode | TypedDocumentNode; + // (undocumented) + variables?: Partial; +} + +// @public +type FetchPolicy = "cache-first" | "network-only" | "cache-only" | "no-cache" | "standby"; + +// Warning: (ae-forgotten-export) The symbol "SingleExecutionResult" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ExecutionPatchResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type FetchResult, TContext = Record, TExtensions = Record> = SingleExecutionResult | ExecutionPatchResult; + +// @public (undocumented) +interface FieldSpecifier { + // (undocumented) + args?: Record; + // (undocumented) + field?: FieldNode; + // (undocumented) + fieldName: string; + // (undocumented) + typename?: string; + // (undocumented) + variables?: Record; +} + +// @public +interface FragmentMap { + // (undocumented) + [fragmentName: string]: FragmentDefinitionNode; +} + +// @public (undocumented) +type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; + +// @public (undocumented) +interface FulfilledPromise extends Promise { + // (undocumented) + status: "fulfilled"; + // (undocumented) + value: TValue; +} + +// Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "SuspenseCache" needs to be exported by the entry point index.d.ts +// Warning: (ae-internal-missing-underscore) The name "getSuspenseCache" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export function getSuspenseCache(client: ApolloClient & { + [suspenseCacheSymbol]?: SuspenseCache; +}): SuspenseCache; + +// Warning: (ae-incompatible-release-tags) The symbol "getWrappedPromise" is marked as @public, but its signature references "QueryRefPromise" which is marked as @internal +// +// @public (undocumented) +export function getWrappedPromise(queryRef: QueryReference): QueryRefPromise; + +// @public (undocumented) +type GraphQLErrors = ReadonlyArray; + +// @public (undocumented) +interface GraphQLRequest> { + // (undocumented) + context?: DefaultContext; + // (undocumented) + extensions?: Record; + // (undocumented) + operationName?: string; + // (undocumented) + query: DocumentNode; + // (undocumented) + variables?: TVariables; +} + +// @public (undocumented) +interface IncrementalPayload { + // (undocumented) + data: TData | null; + // (undocumented) + errors?: ReadonlyArray; + // (undocumented) + extensions?: TExtensions; + // (undocumented) + label?: string; + // Warning: (ae-forgotten-export) The symbol "Path" needs to be exported by the entry point index.d.ts + // + // (undocumented) + path: Path; +} + +// @public (undocumented) +export class InternalQueryReference { + // Warning: (ae-forgotten-export) The symbol "InternalQueryReferenceOptions" needs to be exported by the entry point index.d.ts + constructor(observable: ObservableQuery, options: InternalQueryReferenceOptions); + // Warning: (ae-incompatible-release-tags) The symbol "applyOptions" is marked as @public, but its signature references "QueryRefPromise" which is marked as @internal + // + // (undocumented) + applyOptions(watchQueryOptions: ObservedOptions): QueryRefPromise; + // Warning: (ae-forgotten-export) The symbol "ObservedOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + didChangeOptions(watchQueryOptions: ObservedOptions): boolean; + // (undocumented) + get disposed(): boolean; + // Warning: (ae-forgotten-export) The symbol "FetchMoreOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fetchMore(options: FetchMoreOptions): Promise>; + // (undocumented) + readonly key: QueryKey; + // Warning: (ae-forgotten-export) The symbol "Listener" needs to be exported by the entry point index.d.ts + // + // (undocumented) + listen(listener: Listener): () => void; + // (undocumented) + readonly observable: ObservableQuery; + // Warning: (ae-incompatible-release-tags) The symbol "promise" is marked as @public, but its signature references "QueryRefPromise" which is marked as @internal + // + // (undocumented) + promise: QueryRefPromise; + // (undocumented) + refetch(variables: OperationVariables | undefined): Promise>; + // (undocumented) + result: ApolloQueryResult; + // (undocumented) + retain(): () => void; + // (undocumented) + get watchQueryOptions(): WatchQueryOptions; +} + +// @public (undocumented) +interface InternalQueryReferenceOptions { + // (undocumented) + autoDisposeTimeoutMs?: number; + // (undocumented) + onDispose?: () => void; +} + +// Warning: (ae-forgotten-export) The symbol "InternalRefetchQueryDescriptor" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RefetchQueriesIncludeShorthand" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type InternalRefetchQueriesInclude = InternalRefetchQueryDescriptor[] | RefetchQueriesIncludeShorthand; + +// Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type InternalRefetchQueriesMap = Map, InternalRefetchQueriesResult>; + +// @public (undocumented) +interface InternalRefetchQueriesOptions, TResult> extends Omit, "include"> { + // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesInclude" needs to be exported by the entry point index.d.ts + // + // (undocumented) + include?: InternalRefetchQueriesInclude; + // (undocumented) + removeOptimistic?: string; +} + +// @public (undocumented) +type InternalRefetchQueriesResult = TResult extends boolean ? Promise> : TResult; + +// Warning: (ae-forgotten-export) The symbol "RefetchQueryDescriptor" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type InternalRefetchQueryDescriptor = RefetchQueryDescriptor | QueryOptions; + +// @public (undocumented) +interface InvalidateModifier { + // (undocumented) + [_invalidateModifier]: true; +} + +// @public (undocumented) +const _invalidateModifier: unique symbol; + +// @public (undocumented) +function isReference(obj: any): obj is Reference; + +// Warning: (ae-forgotten-export) The symbol "UnionToIntersection" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UnionForAny" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type IsStrictlyAny = UnionToIntersection> extends never ? true : false; + +// Warning: (ae-incompatible-release-tags) The symbol "Listener" is marked as @public, but its signature references "QueryRefPromise" which is marked as @internal +// +// @public (undocumented) +type Listener = (promise: QueryRefPromise) => void; + +// @public (undocumented) +class LocalState { + // Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts + constructor({ cache, client, resolvers, fragmentMatcher, }: LocalStateOptions); + // (undocumented) + addExportedVariables(document: DocumentNode, variables?: TVars, context?: {}): Promise; + // (undocumented) + addResolvers(resolvers: Resolvers | Resolvers[]): void; + // (undocumented) + clientQuery(document: DocumentNode): DocumentNode | null; + // (undocumented) + getFragmentMatcher(): FragmentMatcher | undefined; + // (undocumented) + getResolvers(): Resolvers; + // (undocumented) + prepareContext(context?: Record): { + cache: ApolloCache; + getCacheKey(obj: StoreObject): string | undefined; + }; + // (undocumented) + runResolvers({ document, remoteResult, context, variables, onlyRunForcedResolvers, }: { + document: DocumentNode | null; + remoteResult: FetchResult; + context?: Record; + variables?: Record; + onlyRunForcedResolvers?: boolean; + }): Promise>; + // (undocumented) + serverQuery(document: DocumentNode): DocumentNode | null; + // (undocumented) + setFragmentMatcher(fragmentMatcher: FragmentMatcher): void; + // (undocumented) + setResolvers(resolvers: Resolvers | Resolvers[]): void; + // (undocumented) + shouldForceResolvers(document: ASTNode): boolean; +} + +// @public (undocumented) +type LocalStateOptions = { + cache: ApolloCache; + client?: ApolloClient; + resolvers?: Resolvers | Resolvers[]; + fragmentMatcher?: FragmentMatcher; +}; + +// @public (undocumented) +type MaybeAsync = T | PromiseLike; + +// @public (undocumented) +class MissingFieldError extends Error { + constructor(message: string, path: MissingTree | Array, query: DocumentNode, variables?: Record | undefined); + // (undocumented) + readonly message: string; + // (undocumented) + readonly missing: MissingTree; + // Warning: (ae-forgotten-export) The symbol "MissingTree" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly path: MissingTree | Array; + // (undocumented) + readonly query: DocumentNode; + // (undocumented) + readonly variables?: Record | undefined; +} + +// @public (undocumented) +type MissingTree = string | { + readonly [key: string]: MissingTree; +}; + +// Warning: (ae-forgotten-export) The symbol "ModifierDetails" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeleteModifier" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "InvalidateModifier" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type Modifier = (value: T, details: ModifierDetails) => T | DeleteModifier | InvalidateModifier; + +// @public (undocumented) +type ModifierDetails = { + DELETE: DeleteModifier; + INVALIDATE: InvalidateModifier; + fieldName: string; + storeFieldName: string; + readField: ReadFieldFunction; + canRead: CanReadFunction; + isReference: typeof isReference; + toReference: ToReferenceFunction; + storage: StorageType; +}; + +// @public (undocumented) +type Modifiers = Record> = Partial<{ + [FieldName in keyof T]: Modifier>>; +}>; + +// @public (undocumented) +interface MutationBaseOptions = ApolloCache> { + awaitRefetchQueries?: boolean; + context?: TContext; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" + errorPolicy?: ErrorPolicy; + // Warning: (ae-forgotten-export) The symbol "OnQueryUpdated" needs to be exported by the entry point index.d.ts + onQueryUpdated?: OnQueryUpdated; + optimisticResponse?: TData | ((vars: TVariables) => TData); + refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloCache" + update?: MutationUpdaterFunction; + // Warning: (ae-forgotten-export) The symbol "MutationQueryReducersMap" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationQueryReducersMap" + updateQueries?: MutationQueryReducersMap; + variables?: TVariables; +} + +// Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type MutationFetchPolicy = Extract; + +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "MutationFetchPolicy" + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; + mutation: DocumentNode | TypedDocumentNode; +} + +// @public (undocumented) +type MutationQueryReducer = (previousResult: Record, options: { + mutationResult: FetchResult; + queryName: string | undefined; + queryVariables: Record; +}) => Record; + +// @public (undocumented) +type MutationQueryReducersMap = { + [queryName: string]: MutationQueryReducer; +}; + +// @public (undocumented) +interface MutationStoreValue { + // (undocumented) + error: Error | null; + // (undocumented) + loading: boolean; + // (undocumented) + mutation: DocumentNode; + // (undocumented) + variables: Record; +} + +// @public (undocumented) +type MutationUpdaterFunction> = (cache: TCache, result: Omit, "context">, options: { + context?: TContext; + variables?: TVariables; +}) => void; + +// @public +enum NetworkStatus { + error = 8, + fetchMore = 3, + loading = 1, + poll = 6, + ready = 7, + refetch = 4, + setVariables = 2 +} + +// @public (undocumented) +interface NextFetchPolicyContext { + // Warning: (ae-forgotten-export) The symbol "WatchQueryFetchPolicy" needs to be exported by the entry point index.d.ts + // + // (undocumented) + initialFetchPolicy: WatchQueryFetchPolicy; + // (undocumented) + observable: ObservableQuery; + // (undocumented) + options: WatchQueryOptions; + // (undocumented) + reason: "after-fetch" | "variables-changed"; +} + +// @public (undocumented) +type NextLink = (operation: Operation) => Observable; + +// @public (undocumented) +type NextResultListener = (method: "next" | "error" | "complete", arg?: any) => any; + +// @public (undocumented) +class ObservableQuery extends Observable> { + constructor({ queryManager, queryInfo, options, }: { + queryManager: QueryManager; + queryInfo: QueryInfo; + options: WatchQueryOptions; + }); + // Warning: (ae-forgotten-export) The symbol "FetchMoreQueryOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + // (undocumented) + getCurrentResult(saveAsLastResult?: boolean): ApolloQueryResult; + // (undocumented) + getLastError(variablesMustMatch?: boolean): ApolloError | undefined; + // (undocumented) + getLastResult(variablesMustMatch?: boolean): ApolloQueryResult | undefined; + // (undocumented) + hasObservers(): boolean; + // (undocumented) + isDifferentFromLastResult(newResult: ApolloQueryResult, variables?: TVariables): boolean | undefined; + // (undocumented) + readonly options: WatchQueryOptions; + // (undocumented) + get query(): TypedDocumentNode; + // (undocumented) + readonly queryId: string; + // (undocumented) + readonly queryName?: string; + refetch(variables?: Partial): Promise>; + // (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + // Warning: (ae-forgotten-export) The symbol "Concast" needs to be exported by the entry point index.d.ts + // + // (undocumented) + reobserveAsConcast(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Concast>; + // (undocumented) + resetLastResults(): void; + // (undocumented) + resetQueryStoreErrors(): void; + // (undocumented) + resubscribeAfterError(onNext: (value: ApolloQueryResult) => void, onError?: (error: any) => void, onComplete?: () => void): Subscription; + // (undocumented) + resubscribeAfterError(observer: Observer>): Subscription; + // (undocumented) + result(): Promise>; + // (undocumented) + setOptions(newOptions: Partial>): Promise>; + setVariables(variables: TVariables): Promise | void>; + // (undocumented) + silentSetOptions(newOptions: Partial>): void; + // (undocumented) + startPolling(pollInterval: number): void; + // (undocumented) + stopPolling(): void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // (undocumented) + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + // (undocumented) + get variables(): TVariables | undefined; +} + +// @public (undocumented) +const OBSERVED_CHANGED_OPTIONS: readonly ["canonizeResults", "context", "errorPolicy", "fetchPolicy", "refetchWritePolicy", "returnPartialData"]; + +// Warning: (ae-forgotten-export) The symbol "OBSERVED_CHANGED_OPTIONS" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type ObservedOptions = Pick; + +// @public (undocumented) +type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; + +// @public (undocumented) +interface Operation { + // (undocumented) + extensions: Record; + // (undocumented) + getContext: () => DefaultContext; + // (undocumented) + operationName: string; + // (undocumented) + query: DocumentNode; + // (undocumented) + setContext: (context: DefaultContext) => DefaultContext; + // (undocumented) + variables: Record; +} + +// @public (undocumented) +type OperationVariables = Record; + +// @public (undocumented) +type Path = ReadonlyArray; + +// @public (undocumented) +interface PendingPromise extends Promise { + // (undocumented) + status: "pending"; +} + +// @public (undocumented) +const PROMISE_SYMBOL: unique symbol; + +// Warning: (ae-forgotten-export) The symbol "PendingPromise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "FulfilledPromise" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "RejectedPromise" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type PromiseWithState = PendingPromise | FulfilledPromise | RejectedPromise; + +// @public (undocumented) +const QUERY_REFERENCE_SYMBOL: unique symbol; + +// @public (undocumented) +class QueryInfo { + constructor(queryManager: QueryManager, queryId?: string); + // (undocumented) + document: DocumentNode | null; + // (undocumented) + getDiff(): Cache_2.DiffResult; + // (undocumented) + graphQLErrors?: ReadonlyArray; + // (undocumented) + init(query: { + document: DocumentNode; + variables: Record | undefined; + networkStatus?: NetworkStatus; + observableQuery?: ObservableQuery; + lastRequestId?: number; + }): this; + // (undocumented) + lastRequestId: number; + // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts + // + // (undocumented) + listeners: Set; + // (undocumented) + markError(error: ApolloError): ApolloError; + // (undocumented) + markReady(): NetworkStatus; + // Warning: (ae-forgotten-export) The symbol "CacheWriteBehavior" needs to be exported by the entry point index.d.ts + // + // (undocumented) + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; + // (undocumented) + networkError?: Error | null; + // (undocumented) + networkStatus?: NetworkStatus; + // (undocumented) + notify(): void; + // (undocumented) + readonly observableQuery: ObservableQuery | null; + // (undocumented) + readonly queryId: string; + // (undocumented) + reset(): void; + // (undocumented) + resetLastWrite(): void; + // (undocumented) + setDiff(diff: Cache_2.DiffResult | null): void; + // (undocumented) + setObservableQuery(oq: ObservableQuery | null): void; + // (undocumented) + stop(): void; + // (undocumented) + stopped: boolean; + // (undocumented) + variables?: Record; +} + +// @public (undocumented) +export interface QueryKey { + // (undocumented) + __queryKey?: string; +} + +// @public (undocumented) +type QueryListener = (queryInfo: QueryInfo) => void; + +// @public (undocumented) +class QueryManager { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { + cache: ApolloCache; + link: ApolloLink; + defaultOptions?: DefaultOptions; + documentTransform?: DocumentTransform; + queryDeduplication?: boolean; + onBroadcast?: () => void; + ssrMode?: boolean; + clientAwareness?: Record; + localState?: LocalState; + assumeImmutableResults?: boolean; + defaultContext?: Partial; + }); + // (undocumented) + readonly assumeImmutableResults: boolean; + // (undocumented) + broadcastQueries(): void; + // (undocumented) + cache: ApolloCache; + // (undocumented) + clearStore(options?: Cache_2.ResetOptions): Promise; + // (undocumented) + readonly defaultContext: Partial; + // Warning: (ae-forgotten-export) The symbol "DefaultOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + defaultOptions: DefaultOptions; + // (undocumented) + readonly documentTransform: DocumentTransform; + // (undocumented) + protected fetchCancelFns: Map any>; + // (undocumented) + fetchQuery(queryId: string, options: WatchQueryOptions, networkStatus?: NetworkStatus): Promise>; + // (undocumented) + generateMutationId(): string; + // (undocumented) + generateQueryId(): string; + // (undocumented) + generateRequestId(): number; + // Warning: (ae-forgotten-export) The symbol "TransformCacheEntry" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getDocumentInfo(document: DocumentNode): TransformCacheEntry; + // (undocumented) + getLocalState(): LocalState; + // (undocumented) + getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getQueryStore(): Record; + // (undocumented) + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; + // (undocumented) + link: ApolloLink; + // (undocumented) + markMutationOptimistic>(optimisticResponse: any, mutation: { + mutationId: string; + document: DocumentNode; + variables?: TVariables; + fetchPolicy?: MutationFetchPolicy; + errorPolicy: ErrorPolicy; + context?: TContext; + updateQueries: UpdateQueries; + update?: MutationUpdaterFunction; + keepRootFields?: boolean; + }): void; + // (undocumented) + markMutationResult>(mutation: { + mutationId: string; + result: FetchResult; + document: DocumentNode; + variables?: TVariables; + fetchPolicy?: MutationFetchPolicy; + errorPolicy: ErrorPolicy; + context?: TContext; + updateQueries: UpdateQueries; + update?: MutationUpdaterFunction; + awaitRefetchQueries?: boolean; + refetchQueries?: InternalRefetchQueriesInclude; + removeOptimistic?: string; + onQueryUpdated?: OnQueryUpdated; + keepRootFields?: boolean; + }, cache?: ApolloCache): Promise>; + // (undocumented) + mutate, TCache extends ApolloCache>({ mutation, variables, optimisticResponse, updateQueries, refetchQueries, awaitRefetchQueries, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }: MutationOptions): Promise>; + // (undocumented) + mutationStore?: { + [mutationId: string]: MutationStoreValue; + }; + // (undocumented) + query(options: QueryOptions, queryId?: string): Promise>; + // (undocumented) + reFetchObservableQueries(includeStandby?: boolean): Promise[]>; + // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "InternalRefetchQueriesMap" needs to be exported by the entry point index.d.ts + // + // (undocumented) + refetchQueries({ updateCache, include, optimistic, removeOptimistic, onQueryUpdated, }: InternalRefetchQueriesOptions, TResult>): InternalRefetchQueriesMap; + // (undocumented) + removeQuery(queryId: string): void; + // (undocumented) + resetErrors(queryId: string): void; + // (undocumented) + setObservableQuery(observableQuery: ObservableQuery): void; + // (undocumented) + readonly ssrMode: boolean; + // (undocumented) + startGraphQLSubscription({ query, fetchPolicy, errorPolicy, variables, context, }: SubscriptionOptions): Observable>; + stop(): void; + // (undocumented) + stopQuery(queryId: string): void; + // (undocumented) + stopQueryInStore(queryId: string): void; + // (undocumented) + transform(document: DocumentNode): DocumentNode; + // (undocumented) + watchQuery(options: WatchQueryOptions): ObservableQuery; +} + +// @public +interface QueryOptions { + canonizeResults?: boolean; + context?: DefaultContext; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" + errorPolicy?: ErrorPolicy; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + fetchPolicy?: FetchPolicy; + notifyOnNetworkStatusChange?: boolean; + partialRefetch?: boolean; + pollInterval?: number; + query: DocumentNode | TypedDocumentNode; + returnPartialData?: boolean; + variables?: TVariables; +} + +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "useBackgroundQuery" +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "useReadQuery" +// +// @public +export interface QueryReference { + // Warning: (ae-incompatible-release-tags) The symbol "[PROMISE_SYMBOL]" is marked as @public, but its signature references "QueryRefPromise" which is marked as @internal + // + // (undocumented) + [PROMISE_SYMBOL]: QueryRefPromise; + // (undocumented) + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // Warning: (ae-forgotten-export) The symbol "DisposeFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + retain: () => DisposeFn; + // (undocumented) + toPromise(): Promise>; +} + +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// Warning: (ae-internal-missing-underscore) The name "QueryRefPromise" should be prefixed with an underscore because the declaration is marked as @internal +// +// @internal (undocumented) +export type QueryRefPromise = PromiseWithState>; + +// @public (undocumented) +type QueryStoreValue = Pick; + +// @public (undocumented) +interface ReadFieldFunction { + // Warning: (ae-forgotten-export) The symbol "ReadFieldOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "SafeReadonly" needs to be exported by the entry point index.d.ts + // + // (undocumented) + (options: ReadFieldOptions): SafeReadonly | undefined; + // (undocumented) + (fieldName: string, from?: StoreObject | Reference): SafeReadonly | undefined; +} + +// Warning: (ae-forgotten-export) The symbol "FieldSpecifier" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface ReadFieldOptions extends FieldSpecifier { + // (undocumented) + from?: StoreObject | Reference; +} + +// @public (undocumented) +interface Reference { + // (undocumented) + readonly __ref: string; +} + +// @public (undocumented) +type RefetchQueriesInclude = RefetchQueryDescriptor[] | RefetchQueriesIncludeShorthand; + +// @public (undocumented) +type RefetchQueriesIncludeShorthand = "all" | "active"; + +// @public (undocumented) +interface RefetchQueriesOptions, TResult> { + // (undocumented) + include?: RefetchQueriesInclude; + // (undocumented) + onQueryUpdated?: OnQueryUpdated | null; + // (undocumented) + optimistic?: boolean; + // (undocumented) + updateCache?: (cache: TCache) => void; +} + +// Warning: (ae-forgotten-export) The symbol "IsStrictlyAny" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type RefetchQueriesPromiseResults = IsStrictlyAny extends true ? any[] : TResult extends boolean ? ApolloQueryResult[] : TResult extends PromiseLike ? U[] : TResult[]; + +// Warning: (ae-forgotten-export) The symbol "RefetchQueriesPromiseResults" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface RefetchQueriesResult extends Promise> { + // (undocumented) + queries: ObservableQuery[]; + // (undocumented) + results: InternalRefetchQueriesResult[]; +} + +// @public (undocumented) +type RefetchQueryDescriptor = string | DocumentNode; + +// @public (undocumented) +type RefetchWritePolicy = "merge" | "overwrite"; + +// @public (undocumented) +interface RejectedPromise extends Promise { + // (undocumented) + reason: unknown; + // (undocumented) + status: "rejected"; +} + +// @public (undocumented) +type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; + +// @public (undocumented) +type Resolver = (rootValue?: any, args?: any, context?: any, info?: { + field: FieldNode; + fragmentMap: FragmentMap; +}) => any; + +// @public (undocumented) +interface Resolvers { + // (undocumented) + [key: string]: { + [field: string]: Resolver; + }; +} + +// @public (undocumented) +type SafeReadonly = T extends object ? Readonly : T; + +// @public (undocumented) +type ServerError = Error & { + response: Response; + result: Record | string; + statusCode: number; +}; + +// @public (undocumented) +type ServerParseError = Error & { + response: Response; + statusCode: number; + bodyText: string; +}; + +// @public (undocumented) +interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { + // (undocumented) + context?: TContext; + // (undocumented) + data?: TData | null; +} + +// @public (undocumented) +type Source = MaybeAsync>; + +// @public (undocumented) +type StorageType = Record; + +// @public (undocumented) +interface StoreObject { + // (undocumented) + [storeFieldName: string]: StoreValue; + // (undocumented) + __typename?: string; +} + +// Warning: (ae-forgotten-export) The symbol "AsStoreObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type StoreObjectValueMaybeReference = StoreVal extends Array> ? StoreVal extends Array ? Item extends Record ? ReadonlyArray | Reference> : never : never : StoreVal extends Record ? AsStoreObject | Reference : StoreVal; + +// @public (undocumented) +type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; + +// @public (undocumented) +type SubscribeToMoreOptions = { + document: DocumentNode | TypedDocumentNode; + variables?: TSubscriptionVariables; + updateQuery?: UpdateQueryFn; + onError?: (error: Error) => void; + context?: DefaultContext; +}; + +// @public (undocumented) +interface SubscriptionOptions { + context?: DefaultContext; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" + errorPolicy?: ErrorPolicy; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + fetchPolicy?: FetchPolicy; + query: DocumentNode | TypedDocumentNode; + variables?: TVariables; +} + +// @public (undocumented) +class SuspenseCache { + constructor(options?: SuspenseCacheOptions); + // (undocumented) + getQueryRef(cacheKey: CacheKey, createObservable: () => ObservableQuery): InternalQueryReference; +} + +// @public (undocumented) +export interface SuspenseCacheOptions { + autoDisposeTimeoutMs?: number; +} + +// @public (undocumented) +const suspenseCacheSymbol: unique symbol; + +// @public (undocumented) +type ToReferenceFunction = (objOrIdOrRef: StoreObject | string | Reference, mergeIntoStore?: boolean) => Reference | undefined; + +// @public (undocumented) +type Transaction = (c: ApolloCache) => void; + +// @public (undocumented) +interface TransformCacheEntry { + // (undocumented) + asQuery: DocumentNode; + // (undocumented) + clientQuery: DocumentNode | null; + // (undocumented) + defaultVars: OperationVariables; + // (undocumented) + hasClientExports: boolean; + // (undocumented) + hasForcedResolvers: boolean; + // (undocumented) + hasNonreactiveDirective: boolean; + // (undocumented) + serverQuery: DocumentNode | null; +} + +// @public (undocumented) +type TransformFn = (document: DocumentNode) => DocumentNode; + +// @public (undocumented) +type UnionForAny = T extends never ? "a" : 1; + +// @public (undocumented) +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; + +// @public (undocumented) +export function unwrapQueryRef(queryRef: QueryReference): InternalQueryReference; + +// @public (undocumented) +type UpdateQueries = MutationOptions["updateQueries"]; + +// @public (undocumented) +type UpdateQueryFn = (previousQueryResult: TData, options: { + subscriptionData: { + data: TSubscriptionData; + }; + variables?: TSubscriptionVariables; +}) => TData; + +// Warning: (ae-incompatible-release-tags) The symbol "updateWrappedQueryRef" is marked as @public, but its signature references "QueryRefPromise" which is marked as @internal +// +// @public (undocumented) +export function updateWrappedQueryRef(queryRef: QueryReference, promise: QueryRefPromise): void; + +// @public (undocumented) +interface UriFunction { + // (undocumented) + (operation: Operation): string; +} + +// @public (undocumented) +type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; + +// @public +interface WatchQueryOptions { + canonizeResults?: boolean; + context?: DefaultContext; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ErrorPolicy" + errorPolicy?: ErrorPolicy; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "FetchPolicy" + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + partialRefetch?: boolean; + pollInterval?: number; + query: DocumentNode | TypedDocumentNode; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "NetworkStatus" + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + +// @public (undocumented) +export function wrapQueryRef(internalQueryRef: InternalQueryReference): QueryReference; + +// Warnings were encountered during analysis: +// +// src/cache/core/types/DataProxy.ts:141:5 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:96:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:97:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:98:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:99:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts +// src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:113:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:114:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:120:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:154:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:395:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/types.ts:154:3 - (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts +// src/core/types.ts:156:3 - (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts +// src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts + +// (No @packageDocumentation comment for this package) + +``` diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 60afb271e76..80ef70d80b1 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -2989,9 +2989,9 @@ interface WriteContext extends ReadMergeModifyContext { // src/core/QueryManager.ts:395:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/watchQueryOptions.ts:260:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:31:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useLoadableQuery.ts:50:5 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:29:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:30:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useLoadableQuery.ts:49:5 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) From 6883f83ffe1a6777b2f51828b81d7729ab28b5fe Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 13:03:54 +0100 Subject: [PATCH 128/133] size-limits --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index fb873241928..9ce3fd3d19f 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": 38820, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32366 } From 9c11e4113278c281ea273ba1d816b9b7ed63f7f4 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 13:11:18 +0100 Subject: [PATCH 129/133] add changeset --- .changeset/ten-bananas-smile.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/ten-bananas-smile.md diff --git a/.changeset/ten-bananas-smile.md b/.changeset/ten-bananas-smile.md new file mode 100644 index 00000000000..78ee594d5f2 --- /dev/null +++ b/.changeset/ten-bananas-smile.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Changes to bundling to prevent duplication of shipped code From e6600277079574351a905bcb5a0fd320cb275998 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 14:26:41 +0100 Subject: [PATCH 130/133] update size-limits --- .size-limits.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limits.json b/.size-limits.json index fb873241928..e3e9a1c32a2 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": 38821, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32366 } From a319ca3ca73b6e31d9e79fad393a8a2e635a70c9 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 14:27:55 +0100 Subject: [PATCH 131/133] udpate size-limits --- .size-limits.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limits.json b/.size-limits.json index e3e9a1c32a2..9ce3fd3d19f 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 38821, + "dist/apollo-client.min.cjs": 38820, "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32366 } From 38881091fafe183dcc8e06be4d34190f4040e802 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Tue, 12 Dec 2023 14:30:27 +0100 Subject: [PATCH 132/133] update exports --- src/__tests__/__snapshots__/exports.ts.snap | 11 +++++++++++ src/__tests__/exports.ts | 2 ++ 2 files changed, 13 insertions(+) diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index f67bdb6f92d..79e33b4f1b5 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -333,6 +333,17 @@ Array [ ] `; +exports[`exports of public entry points @apollo/client/react/internal 1`] = ` +Array [ + "InternalQueryReference", + "getSuspenseCache", + "getWrappedPromise", + "unwrapQueryRef", + "updateWrappedQueryRef", + "wrapQueryRef", +] +`; + exports[`exports of public entry points @apollo/client/react/parser 1`] = ` Array [ "DocumentType", diff --git a/src/__tests__/exports.ts b/src/__tests__/exports.ts index dc46f2498ad..9ebfa190368 100644 --- a/src/__tests__/exports.ts +++ b/src/__tests__/exports.ts @@ -22,6 +22,7 @@ import * as linkSubscriptions from "../link/subscriptions"; import * as linkUtils from "../link/utils"; import * as linkWS from "../link/ws"; import * as react from "../react"; +import * as reactInternals from "../react/internal"; import * as reactComponents from "../react/components"; import * as reactContext from "../react/context"; import * as reactHOC from "../react/hoc"; @@ -67,6 +68,7 @@ describe("exports of public entry points", () => { check("@apollo/client/link/utils", linkUtils); check("@apollo/client/link/ws", linkWS); check("@apollo/client/react", react); + check("@apollo/client/react/internal", reactInternals); check("@apollo/client/react/components", reactComponents); check("@apollo/client/react/context", reactContext); check("@apollo/client/react/hoc", reactHOC); From 48726fe79bad903a2c6256c43e675f9f8476b341 Mon Sep 17 00:00:00 2001 From: Lenz Weber-Tronic Date: Wed, 13 Dec 2023 10:44:22 +0100 Subject: [PATCH 133/133] remove changeset --- .changeset/ten-bananas-smile.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/ten-bananas-smile.md diff --git a/.changeset/ten-bananas-smile.md b/.changeset/ten-bananas-smile.md deleted file mode 100644 index 78ee594d5f2..00000000000 --- a/.changeset/ten-bananas-smile.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Changes to bundling to prevent duplication of shipped code