diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 7acf9ba2c5a..674e60b090c 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"; @@ -2258,9 +2345,9 @@ interface WatchQueryOptions 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) @@ -2149,9 +2174,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 5a22a0eefdd..80ef70d80b1 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"; @@ -2902,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) diff --git a/.changeset/rare-snakes-melt.md b/.changeset/rare-snakes-melt.md new file mode 100644 index 00000000000..6757b401a47 --- /dev/null +++ b/.changeset/rare-snakes-melt.md @@ -0,0 +1,24 @@ +--- +"@apollo/client": minor +--- + +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 +} +``` 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 }} 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 } 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/config/jest.config.js b/config/jest.config.js index a45df96fc48..6851e2a6e06 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -36,6 +36,8 @@ 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__/useQueryRefHandlers.test.tsx", + "src/react/query-preloader/__tests__/createQueryPreloader.test.tsx", ]; const tsStandardConfig = { diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 70229c88a17..79e33b4f1b5 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", @@ -62,6 +63,7 @@ Array [ "useLoadableQuery", "useMutation", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", @@ -265,6 +267,7 @@ Array [ "ApolloConsumer", "ApolloProvider", "DocumentType", + "createQueryPreloader", "getApolloContext", "operationName", "parser", @@ -277,6 +280,7 @@ Array [ "useLoadableQuery", "useMutation", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", @@ -321,6 +325,7 @@ Array [ "useLoadableQuery", "useMutation", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", @@ -328,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); 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 */ 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/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 85b857e47a5..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 { unwrapQueryRef, QueryReference } from "../../cache/QueryReference"; +import { QueryReference } from "../../internal/index.js"; 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" }, @@ -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/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx new file mode 100644 index 00000000000..5ce723afa4c --- /dev/null +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -0,0 +1,1886 @@ +import React from "react"; +import { act, render, screen } from "@testing-library/react"; +import { + ApolloClient, + InMemoryCache, + NetworkStatus, + TypedDocumentNode, + gql, +} from "../../../core"; +import { MockLink, MockedResponse } from "../../../testing"; +import { + PaginatedCaseData, + SimpleCaseData, + createProfiler, + renderWithClient, + usePaginatedCase, + useSimpleCase, + useTrackRenders, +} from "../../../testing/internal"; +import { useQueryRefHandlers } from "../useQueryRefHandlers"; +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 "../../internal/index.js"; +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(); + + 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 = 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 + useQueryRefHandlers(queryRef); + + return ( + }> + + + ); + } + + const { rerender } = renderWithClient(, { client, wrapper: Profiler }); + + { + 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, + }); + } +}); + +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 = preloadQuery(query); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook() { + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + function App() { + useTrackRenders(); + const { refetch } = useQueryRefHandlers(queryRef); + + 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: { 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, + }); + } +}); + +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 = 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 } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // 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(); +}); + +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 = 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 } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // 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(); +}); + +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 = 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 } = useQueryRefHandlers(queryRef); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + renderWithClient(, { client, wrapper: Profiler }); + + // 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(); +}); + +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 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 } = useQueryRefHandlers(queryRef); + const [isPending, startTransition] = React.useTransition(); + + Profiler.mergeSnapshot({ isPending }); + + return ( + <> + + }> + + + + ); + } + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function Todo() { + useTrackRenders(); + const result = useReadQuery(queryRef); + const { todo } = result.data; + + Profiler.mergeSnapshot({ result }); + + return ( +
+ {todo.name} + {todo.completed && " (completed)"} +
+ ); + } + + render(, { wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + const button = screen.getByText("Refetch"); + await act(() => user.click(button)); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, Todo]); + expect(snapshot).toEqual({ + isPending: true, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, Todo]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("`refetch` works with startTransition from useBackgroundQuery and usePreloadedQueryHandlers", async () => { + const { query, mocks: defaultMocks } = useSimpleCase(); + + const user = userEvent.setup(); + + const mocks = [ + defaultMocks[0], + { + request: { query }, + result: { data: { greeting: "Hello again" } }, + delay: 20, + }, + { + request: { query }, + result: { data: { greeting: "You again?" } }, + delay: 20, + }, + ]; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + useBackgroundQueryIsPending: false, + usePreloadedQueryHandlersIsPending: false, + result: null as UseReadQueryResult | null, + }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook({ + queryRef, + }: { + queryRef: QueryReference; + }) { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const { refetch } = useQueryRefHandlers(queryRef); + + Profiler.mergeSnapshot({ + usePreloadedQueryHandlersIsPending: isPending, + result: useReadQuery(queryRef), + }); + + return ( + + ); + } + + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const [queryRef, { refetch }] = 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: { 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("refetches from 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 } = 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: { 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("refetches from 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 } = 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: { 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("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, + }); + } +}); + +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, + }); + } +}); + +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(); +}); diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 8a725261f40..78fc82c61f4 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 { 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/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 47484ad45b8..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< @@ -51,7 +50,8 @@ export function useBackgroundQuery< DeepPartial | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial - : TData + : TData, + TVariables > | (TOptions["skip"] extends boolean ? undefined : never) ), @@ -68,7 +68,7 @@ export function useBackgroundQuery< errorPolicy: "ignore" | "all"; } ): [ - QueryReference | undefined>, + QueryReference | undefined, TVariables>, UseBackgroundQueryResult, ]; @@ -81,7 +81,7 @@ export function useBackgroundQuery< errorPolicy: "ignore" | "all"; } ): [ - QueryReference, + QueryReference, UseBackgroundQueryResult, ]; @@ -95,7 +95,7 @@ export function useBackgroundQuery< returnPartialData: true; } ): [ - QueryReference> | undefined, + QueryReference, TVariables> | undefined, UseBackgroundQueryResult, ]; @@ -108,7 +108,7 @@ export function useBackgroundQuery< returnPartialData: true; } ): [ - QueryReference>, + QueryReference, TVariables>, UseBackgroundQueryResult, ]; @@ -121,7 +121,7 @@ export function useBackgroundQuery< skip: boolean; } ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ]; @@ -131,7 +131,10 @@ export function useBackgroundQuery< >( query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer -): [QueryReference, UseBackgroundQueryResult]; +): [ + QueryReference, + UseBackgroundQueryResult, +]; export function useBackgroundQuery< TData = unknown, @@ -152,7 +155,7 @@ export function useBackgroundQuery< returnPartialData: true; }) ): [ - QueryReference> | undefined, + QueryReference, TVariables> | undefined, UseBackgroundQueryResult, ]; @@ -163,7 +166,7 @@ export function useBackgroundQuery< query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ]; @@ -177,7 +180,7 @@ export function useBackgroundQuery< Partial>) | BackgroundQueryHookOptionsNoInfer = Object.create(null) ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ] { const client = useApolloClient(options.client); @@ -208,7 +211,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 282988d8a16..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 = ( @@ -43,7 +42,7 @@ export type UseLoadableQueryResult< TVariables extends OperationVariables = OperationVariables, > = [ LoadQueryFunction, - QueryReference | null, + QueryReference | null, { fetchMore: FetchMoreFunction; refetch: RefetchFunction; @@ -119,11 +118,12 @@ 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]; + const internalQueryRef = queryRef && unwrapQueryRef(queryRef); if (queryRef && internalQueryRef?.didChangeOptions(watchQueryOptions)) { const promise = internalQueryRef.applyOptions(watchQueryOptions); diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts new file mode 100644 index 00000000000..1a2443af362 --- /dev/null +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -0,0 +1,68 @@ +import * as React from "rehackt"; +import { + unwrapQueryRef, + updateWrappedQueryRef, + wrapQueryRef, +} 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"; + +export interface UseQueryRefHandlersResult< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> { + refetch: RefetchFunction; + fetchMore: FetchMoreFunction; +} + +export function useQueryRefHandlers< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + queryRef: QueryReference +): UseQueryRefHandlersResult { + 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 + if (previousQueryRef !== queryRef) { + setPreviousQueryRef(queryRef); + setWrappedQueryRef(queryRef); + } else { + updateWrappedQueryRef( + queryRef, + wrappedQueryRef.toPromise() as QueryRefPromise + ); + } + + const refetch: RefetchFunction = React.useCallback( + (variables) => { + const promise = internalQueryRef.refetch(variables); + + setWrappedQueryRef(wrapQueryRef(internalQueryRef)); + + return promise; + }, + [internalQueryRef] + ); + + const fetchMore: FetchMoreFunction = React.useCallback( + (options) => { + const promise = internalQueryRef.fetchMore( + options as FetchMoreQueryOptions + ); + + setWrappedQueryRef(wrapQueryRef(internalQueryRef)); + + return promise; + }, + [internalQueryRef] + ); + + return { refetch, fetchMore }; +} diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index f2320aa58ea..cc233d860bf 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -1,14 +1,12 @@ 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"; 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 +36,30 @@ export interface UseReadQueryResult { export function useReadQuery( queryRef: QueryReference ): UseReadQueryResult { - const [internalQueryRef, getPromise] = React.useMemo( + const internalQueryRef = React.useMemo( () => unwrapQueryRef(queryRef), [queryRef] ); + React.useEffect(() => { + if (!internalQueryRef.disposed) { + return internalQueryRef.retain(); + } + }, [internalQueryRef]); + + if (__DEV__) { + 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 may occur when calling 'dispose' before passing the queryRef to 'useReadQuery'. + +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; + } + } + const promise = useSyncExternalStore( React.useCallback( (forceUpdate) => { @@ -53,8 +70,8 @@ export function useReadQuery( }, [internalQueryRef] ), - getPromise, - getPromise + () => queryRef.toPromise(), + () => queryRef.toPromise() ); const result = __use(promise); 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 784046d950b..16fd78623a2 100644 --- a/src/react/index.ts +++ b/src/react/index.ts @@ -13,4 +13,11 @@ export * from "./hooks/index.js"; export type { IDocumentDefinition } from "./parser/index.js"; export { DocumentType, operationName, parser } from "./parser/index.js"; -export * from "./types/types.js"; +export type { + PreloadQueryOptions, + PreloadQueryFetchPolicy, + PreloadQueryFunction, +} from "./query-preloader/createQueryPreloader.js"; +export { createQueryPreloader } from "./query-preloader/createQueryPreloader.js"; + +export type * from "./types/types.js"; diff --git a/src/react/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts similarity index 83% rename from src/react/cache/QueryReference.ts rename to src/react/internal/cache/QueryReference.ts index 97025c37335..5243080f3df 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -5,24 +5,28 @@ 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 type { useBackgroundQuery, useReadQuery } from "../../hooks/index.js"; +import { wrapPromiseWithState } from "../../../utilities/index.js"; +import { invariant } from "../../../utilities/globals/index.js"; -type QueryRefPromise = PromiseWithState>; +/** @internal */ +export type QueryRefPromise = PromiseWithState>; type Listener = (promise: QueryRefPromise) => void; +type DisposeFn = () => void; + type FetchMoreOptions = Parameters< ObservableQuery["fetchMore"] >[0]; @@ -35,9 +39,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 { +export interface QueryReference { readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; [PROMISE_SYMBOL]: QueryRefPromise; + retain: () => DisposeFn; + toPromise(): Promise>; } interface InternalQueryReferenceOptions { @@ -45,30 +51,32 @@ interface InternalQueryReferenceOptions { autoDisposeTimeoutMs?: number; } -export function wrapQueryRef( +export function wrapQueryRef( internalQueryRef: InternalQueryReference -): QueryReference { +): 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]; + }, + retain: () => internalQueryRef.retain(), [QUERY_REFERENCE_SYMBOL]: internalQueryRef, [PROMISE_SYMBOL]: internalQueryRef.promise, }; } +export function getWrappedPromise(queryRef: QueryReference) { + return queryRef[PROMISE_SYMBOL]; +} + 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 - : queryRef[PROMISE_SYMBOL], - ]; +): InternalQueryReference { + return queryRef[QUERY_REFERENCE_SYMBOL]; } export function updateWrappedQueryRef( @@ -103,6 +111,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; @@ -110,7 +119,7 @@ export class InternalQueryReference { private references = 0; constructor( - observable: ObservableQuery, + observable: ObservableQuery, options: InternalQueryReferenceOptions ) { this.handleNext = this.handleNext.bind(this); @@ -171,13 +180,27 @@ export class InternalQueryReference { return this.observable.options; } + get disposed() { + return this.__disposed; + } + retain() { this.references++; 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) { + if (disposed || this.disposed) { return; } @@ -242,6 +265,7 @@ export class InternalQueryReference { } private dispose() { + this.__disposed = true; this.subscription.unsubscribe(); this.onDispose(); } 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/internal/cache/__tests__/QueryReference.test.ts b/src/react/internal/cache/__tests__/QueryReference.test.ts new file mode 100644 index 00000000000..ed55902a3b9 --- /dev/null +++ b/src/react/internal/cache/__tests__/QueryReference.test.ts @@ -0,0 +1,28 @@ +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 () => { + using _consoleSpy = spyOnConsole("warn"); + const { query, mocks } = useSimpleCase(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(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") + ); +}); 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/__tests__/createQueryPreloader.test.tsx b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx new file mode 100644 index 00000000000..e9e1c2350c6 --- /dev/null +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -0,0 +1,1624 @@ +import React, { Suspense } from "react"; +import { createQueryPreloader } from "../createQueryPreloader"; +import { + ApolloClient, + ApolloError, + ApolloLink, + InMemoryCache, + NetworkStatus, + TypedDocumentNode, + gql, +} from "../../../core"; +import { + MockLink, + MockSubscriptionLink, + MockedResponse, + wait, +} from "../../../testing"; +import { expectTypeOf } from "expect-type"; +import { QueryReference, unwrapQueryRef } from "../../internal/index.js"; +import { DeepPartial, Observable } from "../../../utilities"; +import { + SimpleCaseData, + createProfiler, + spyOnConsole, + useSimpleCase, + useTrackRenders, + useVariablesCase, +} from "../../../testing/internal"; +import { ApolloProvider } from "../../context"; +import { act, render, renderHook } 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({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); +} + +function renderDefaultTestApp({ + client, + queryRef, +}: { + client: ApolloClient; + queryRef: QueryReference; +}) { + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + error: null as Error | null, + }, + }); + + 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" }); + Profiler.mergeSnapshot({ error }); + + return null; + } + + function App() { + useTrackRenders({ name: "App" }); + + return ( + + }> + + + + ); + } + + const utils = render(, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + 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); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + + 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, + }); + } +}); + +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 queryRef = preloadQuery(query, { + variables: { id: "1" }, + }); + + 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: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("tears down the query when calling dispose", async () => { + const { query, mocks } = useSimpleCase(); + const client = createDefaultClient(mocks); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + const dispose = queryRef.retain(); + + 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); +}); + +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); + + expect(queryRef).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + 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); + + expect(queryRef).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + jest.useRealTimers(); +}); + +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); + + expect(queryRef).not.toBeDisposed(); + + jest.useRealTimers(); + + unmount(); + + await wait(0); + + expect(queryRef).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +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()); + + expect(queryRef).not.toBeDisposed(); + + unmount(); + await wait(0); + + expect(queryRef).not.toBeDisposed(); + + 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()); + + expect(queryRef).not.toBeDisposed(); + + dispose(); + await wait(0); + + expect(queryRef).not.toBeDisposed(); + + unmount(); + await wait(0); + + expect(queryRef).toBeDisposed(); +}); + +test("useReadQuery warns when called with a disposed queryRef", async () => { + using _consoleSpy = spyOnConsole("warn"); + const { query, mocks } = useSimpleCase(); + const client = createDefaultClient(mocks); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + const dispose = queryRef.retain(); + + await queryRef.toPromise(); + + dispose(); + + await wait(0); + + const { rerender } = renderHook(() => useReadQuery(queryRef)); + + 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); +}); + +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); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query); + + 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, + }); + } + + 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, + }); + } +}); + +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 = 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, + }); + } +}); + +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 = 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({}); +}); + +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 = 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, + }); + } +}); + +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 = 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(); +}); + +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 = 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(); +}); + +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 preloadQuery = createQueryPreloader(client); + const queryRef = 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(["ErrorFallback"]); + expect(snapshot.error).toEqual( + new ApolloError({ graphQLErrors: [new GraphQLError("Oops")] }) + ); + } +}); + +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 preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { errorPolicy: "all" }); + + 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: undefined, + error: new ApolloError({ graphQLErrors: [new GraphQLError("Oops")] }), + networkStatus: NetworkStatus.error, + }); + expect(snapshot.error).toEqual(null); + } +}); + +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"); + const { query } = useSimpleCase(); + const mocks = [ + { request: { query }, result: { errors: [new GraphQLError("Oops")] } }, + ]; + const client = createDefaultClient(mocks); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { errorPolicy: "ignore" }); + + 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: undefined, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + expect(snapshot.error).toEqual(null); + } +}); + +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 = preloadQuery(query, { + context: { valueA: "A", valueB: "B" }, + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + // 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, + }); +}); + +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 = preloadQuery(query); + const queryRef2 = preloadQuery(query); + const queryRef3 = 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); +}); + +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 = createDefaultClient(mocks); + + client.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + variables: { id: "1" }, + }); + + const preloadQuery = createQueryPreloader(client); + const queryRef = preloadQuery(query, { + variables: { id: "1" }, + returnPartialData: true, + }); + + const { Profiler } = renderDefaultTestApp({ client, queryRef }); + + { + 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, + }); + } +}); + +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 = 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]); +}); + +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 = 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]); +}); + +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 = 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, + }); + } +}); + +describe.skip("type tests", () => { + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink([]), + }); + const preloadQuery = createQueryPreloader(client); + + test("variables are optional and can be anything with untyped DocumentNode", () => { + const query = gql``; + + preloadQuery(query); + preloadQuery(query, { variables: {} }); + preloadQuery(query, { returnPartialData: true, variables: {} }); + preloadQuery(query, { variables: { foo: "bar" } }); + preloadQuery(query, { variables: { foo: "bar", bar: 2 } }); + }); + + 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", () => { + 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: {} }); + 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`", () => { + 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, { 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", () => { + 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: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + returnPartialData: true, + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + test("enforces required variables", () => { + type Data = { character: string }; + type Variables = { id: string }; + const query: TypedDocumentNode = gql``; + + // @ts-expect-error missing variables option + preloadQuery(query); + // @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: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + 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, { + 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: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + test("requires variables with mixed TVariables", () => { + 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); + // @ts-expect-error missing variables argument + 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" } }); + 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: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + 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, { + 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: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + preloadQuery(query, { + variables: { + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + 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 new file mode 100644 index 00000000000..0f0b3a0d40d --- /dev/null +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -0,0 +1,137 @@ +import type { + ApolloClient, + DefaultContext, + DocumentNode, + ErrorPolicy, + OperationVariables, + RefetchWritePolicy, + TypedDocumentNode, + WatchQueryFetchPolicy, + WatchQueryOptions, +} from "../../core/index.js"; +import { canonicalStringify } from "../../utilities/index.js"; +import type { + DeepPartial, + OnlyRequiredProperties, +} from "../../utilities/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 = + [TVariables] extends [never] ? { variables?: Record } + : {} extends OnlyRequiredProperties ? { variables?: TVariables } + : { variables: TVariables }; + +export type PreloadQueryFetchPolicy = Extract< + WatchQueryFetchPolicy, + "cache-first" | "network-only" | "no-cache" | "cache-and-network" +>; + +export type PreloadQueryOptions< + TVariables extends OperationVariables = OperationVariables, +> = { + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: PreloadQueryFetchPolicy; + queryKey?: string | number | any[]; + returnPartialData?: boolean; + refetchWritePolicy?: RefetchWritePolicy; +} & VariablesOption; + +type PreloadQueryOptionsArg< + TVariables extends OperationVariables, + TOptions = unknown, +> = [TVariables] extends [never] ? + [options?: PreloadQueryOptions & TOptions] +: {} extends OnlyRequiredProperties ? + [ + options?: PreloadQueryOptions> & + Omit, + ] +: [ + options: PreloadQueryOptions> & + Omit, + ]; + +export interface PreloadQueryFunction { + < + TData, + TVariables extends OperationVariables, + TOptions extends Omit, + >( + query: DocumentNode | TypedDocumentNode, + ...[options]: PreloadQueryOptionsArg, TOptions> + ): QueryReference< + TOptions["errorPolicy"] extends "ignore" | "all" ? + TOptions["returnPartialData"] extends true ? + DeepPartial | undefined + : TData | undefined + : TOptions["returnPartialData"] extends true ? DeepPartial + : TData, + TVariables + >; + + ( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + } + ): QueryReference | undefined, TVariables>; + + ( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & { + errorPolicy: "ignore" | "all"; + } + ): QueryReference; + + ( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & { + returnPartialData: true; + } + ): QueryReference, TVariables>; + + ( + 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) + ): QueryReference { + const { 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) + ); + + return wrapQueryRef(queryRef); + } + + return preloadQuery; +} diff --git a/src/react/types/types.ts b/src/react/types/types.ts index f6f7af613aa..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 "../cache/QueryReference.js"; +export type { QueryReference } from "../internal/index.js"; /* Common types */ diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index 73a9a00ff0e..bf2faaec83f 100644 --- a/src/testing/internal/index.ts +++ b/src/testing/internal/index.ts @@ -1,3 +1,22 @@ export * from "./profile/index.js"; export * from "./disposables/index.js"; export { ObservableStream } from "./ObservableStream.js"; + +export type { + SimpleCaseData, + PaginatedCaseData, + PaginatedCaseVariables, + VariablesCaseData, + VariablesCaseVariables, +} from "./scenarios/index.js"; +export { + useSimpleCase, + useVariablesCase, + usePaginatedCase, +} 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..c47a533d09c --- /dev/null +++ b/src/testing/internal/renderHelpers.tsx @@ -0,0 +1,70 @@ +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, + { + wrapper: Wrapper = React.Fragment, + ...renderOptions + }: RenderWithMocksOptions +) { + return render(ui, { + ...renderOptions, + wrapper: ({ children }) => { + return ( + + {children} + + ); + }, + }); +} diff --git a/src/testing/internal/scenarios/index.ts b/src/testing/internal/scenarios/index.ts new file mode 100644 index 00000000000..13bb4da1263 --- /dev/null +++ b/src/testing/internal/scenarios/index.ts @@ -0,0 +1,103 @@ +import { ApolloLink, Observable, gql } from "../../../core/index.js"; +import type { TypedDocumentNode } from "../../../core/index.js"; +import type { 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 = + 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 }; +} + +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 }; +} 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..beab7c05528 --- /dev/null +++ b/src/testing/matchers/toBeDisposed.ts @@ -0,0 +1,35 @@ +import type { MatcherFunction } from "expect"; +import type { QueryReference } from "../../react/index.js"; +import { + InternalQueryReference, + unwrapQueryRef, +} from "../../react/internal/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, + }); + + if (!isQueryRef(queryRef)) { + throw new Error(`\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"}.`; + }, + }; +}; 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< [