diff --git a/.api-reports/api-report-cache.md b/.api-reports/api-report-cache.md index 857db061da9..7eed64d3052 100644 --- a/.api-reports/api-report-cache.md +++ b/.api-reports/api-report-cache.md @@ -30,6 +30,10 @@ export abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -125,7 +129,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -195,9 +199,9 @@ export const cacheSlot: { withValue(value: ApolloCache, callback: (this: TThis, ...args: TArgs) => TResult, args?: TArgs | undefined, thisArg?: TThis | undefined): TResult; }; -// @public (undocumented) +// @public export const canonicalStringify: ((value: any) => string) & { - reset: typeof resetCanonicalStringify; + reset(): void; }; // @public (undocumented) @@ -230,12 +234,14 @@ export namespace DataProxy { } // (undocumented) export interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; } // (undocumented) export interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -332,7 +338,10 @@ export abstract class EntityStore implements NormalizedCache { has(dataId: string): boolean; // (undocumented) protected lookup(dataId: string, dependOnExistence?: boolean): StoreObject | undefined; - // (undocumented) + makeCacheKey(document: DocumentNode, callback: Cache_2.WatchCallback, details: string): object; + makeCacheKey(selectionSet: SelectionSetNode, parent: string | StoreObject, varString: string | undefined, canonizeResults: boolean): object; + makeCacheKey(field: FieldNode, array: readonly any[], varString: string | undefined): object; + // @deprecated (undocumented) makeCacheKey(...args: any[]): object; // (undocumented) merge(older: string | StoreObject, newer: StoreObject | string): void; @@ -466,9 +475,38 @@ export interface FragmentRegistryAPI { // (undocumented) register(...fragments: DocumentNode[]): this; // (undocumented) + resetCaches(): void; + // (undocumented) transform(document: D): D; } +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getInMemoryCacheMemoryInternals: (() => { + addTypenameDocumentTransform: { + cache: number; + }[]; + inMemoryCache: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + }; + fragmentRegistry: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + }; + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + // @public (undocumented) export type IdGetter = (value: IdGetterObj) => string | undefined; @@ -508,6 +546,10 @@ export class InMemoryCache extends ApolloCache { resetResultCache?: boolean; resetResultIdentities?: boolean; }): string[]; + // Warning: (ae-forgotten-export) The symbol "getInMemoryCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -540,13 +582,13 @@ export class InMemoryCache extends ApolloCache { // @public (undocumented) export interface InMemoryCacheConfig extends ApolloReducerConfig { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) fragments?: FragmentRegistryAPI; // (undocumented) possibleTypes?: PossibleTypesMap; - // (undocumented) + // @deprecated (undocumented) resultCacheMaxSize?: number; // (undocumented) resultCaching?: boolean; @@ -840,9 +882,6 @@ export interface Reference { readonly __ref: string; } -// @public (undocumented) -function resetCanonicalStringify(): void; - // @public (undocumented) type SafeReadonly = T extends object ? Readonly : T; @@ -931,11 +970,10 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // -// src/cache/inmemory/object-canon.ts:203:32 - (ae-forgotten-export) The symbol "resetCanonicalStringify" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:98:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:92:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index ba90cf855d1..24cf06f5a60 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -20,7 +20,6 @@ import { InvariantError } from 'ts-invariant'; import { Observable } from 'zen-observable-ts'; import type { Subscription as ObservableSubscription } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import { print as print_3 } from 'graphql'; import { resetCaches } from 'graphql-tag'; import type { SelectionSetNode } from 'graphql'; import { setVerbosity as setLogVerbosity } from 'ts-invariant'; @@ -47,6 +46,10 @@ export abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -98,11 +101,15 @@ export class ApolloClient implements DataProxy { cache: ApolloCache; clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; // (undocumented) @@ -141,12 +148,13 @@ export interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + // (undocumented) + defaultContext?: Partial; defaultOptions?: DefaultOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; link?: ApolloLink; name?: string; @@ -218,10 +226,16 @@ export class ApolloLink { static execute(link: ApolloLink, operation: GraphQLRequest): Observable; // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // (undocumented) @@ -239,14 +253,18 @@ export interface ApolloPayloadResult, TExtensions = } // @public (undocumented) -export type ApolloQueryResult = { +export interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public (undocumented) export type ApolloReducerConfig = { @@ -319,7 +337,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -465,12 +483,14 @@ export namespace DataProxy { } // (undocumented) export interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; } // (undocumented) export interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -555,14 +575,17 @@ export class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -572,9 +595,7 @@ export type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -628,7 +649,10 @@ abstract class EntityStore implements NormalizedCache { has(dataId: string): boolean; // (undocumented) protected lookup(dataId: string, dependOnExistence?: boolean): StoreObject | undefined; - // (undocumented) + makeCacheKey(document: DocumentNode, callback: Cache_2.WatchCallback, details: string): object; + makeCacheKey(selectionSet: SelectionSetNode, parent: string | StoreObject, varString: string | undefined, canonizeResults: boolean): object; + makeCacheKey(field: FieldNode, array: readonly any[], varString: string | undefined): object; + // @deprecated (undocumented) makeCacheKey(...args: any[]): object; // (undocumented) merge(older: string | StoreObject, newer: StoreObject | string): void; @@ -744,9 +768,7 @@ export interface FetchMoreOptions export interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -844,6 +866,8 @@ interface FragmentRegistryAPI { // (undocumented) register(...fragments: DocumentNode[]): this; // (undocumented) + resetCaches(): void; + // (undocumented) transform(document: D): D; } @@ -856,6 +880,68 @@ export function fromError(errorValue: any): Observable; // @public (undocumented) export function fromPromise(promise: Promise): Observable; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + +// @internal +const getInMemoryCacheMemoryInternals: (() => { + addTypenameDocumentTransform: { + cache: number; + }[]; + inMemoryCache: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + }; + fragmentRegistry: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + }; + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + export { gql } // @public (undocumented) @@ -931,6 +1017,15 @@ export interface IdGetterObj extends Object { _id?: string; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) export interface IncrementalPayload { // (undocumented) @@ -969,6 +1064,10 @@ export class InMemoryCache extends ApolloCache { resetResultCache?: boolean; resetResultIdentities?: boolean; }): string[]; + // Warning: (ae-forgotten-export) The symbol "getInMemoryCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -1001,7 +1100,7 @@ export class InMemoryCache extends ApolloCache { // @public (undocumented) export interface InMemoryCacheConfig extends ApolloReducerConfig { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // Warning: (ae-forgotten-export) The symbol "FragmentRegistryAPI" needs to be exported by the entry point index.d.ts // @@ -1009,7 +1108,7 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { fragments?: FragmentRegistryAPI; // (undocumented) possibleTypes?: PossibleTypesMap; - // (undocumented) + // @deprecated (undocumented) resultCacheMaxSize?: number; // (undocumented) resultCaching?: boolean; @@ -1248,7 +1347,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; update?: MutationUpdaterFunction; updateQueries?: MutationQueryReducersMap; @@ -1258,12 +1359,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export interface MutationOptions = ApolloCache> extends MutationBaseOptions { - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +export interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1281,6 +1380,14 @@ export type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1293,7 +1400,7 @@ interface MutationStoreValue { variables: Record; } -// @public (undocumented) +// @public @deprecated (undocumented) export type MutationUpdaterFn = (cache: ApolloCache, mutationResult: FetchResult) => void; @@ -1385,7 +1492,6 @@ export class ObservableQuery; }); - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1417,6 +1523,8 @@ export class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1432,15 +1540,10 @@ export class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } @@ -1534,7 +1637,9 @@ export type PossibleTypesMap = { }; // @public (undocumented) -const print_2: typeof print_3; +const print_2: ((ast: ASTNode) => string) & { + reset(): void; +}; // @public (undocumented) interface Printer { @@ -1572,7 +1677,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1586,6 +1691,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1604,7 +1711,7 @@ export type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1615,6 +1722,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1625,6 +1733,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly defaultContext: Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) readonly documentTransform: DocumentTransform; @@ -1651,7 +1761,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1665,7 +1777,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1718,11 +1830,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated canonizeResults?: boolean; context?: DefaultContext; errorPolicy?: ErrorPolicy; fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1899,6 +2013,26 @@ export type ServerParseError = Error & { export { setLogVerbosity } +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) export interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -2043,22 +2177,11 @@ export interface UriFunction { // @public (undocumented) export type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public -export interface WatchQueryOptions { - canonizeResults?: boolean; - context?: DefaultContext; - errorPolicy?: ErrorPolicy; - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +export interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - variables?: TVariables; } // @public (undocumented) @@ -2095,16 +2218,17 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // -// src/cache/inmemory/policies.ts:98:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" 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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:92:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-link_batch-http.md b/.api-reports/api-report-link_batch-http.md index c7952979747..ce5dded3739 100644 --- a/.api-reports/api-report-link_batch-http.md +++ b/.api-reports/api-report-link_batch-http.md @@ -10,7 +10,6 @@ import type { ExecutionResult } from 'graphql'; import type { GraphQLError } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import { print as print_3 } from 'graphql'; // @public (undocumented) class ApolloLink { @@ -30,12 +29,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -208,7 +213,9 @@ interface Operation { type Path = ReadonlyArray; // @public (undocumented) -const print_2: typeof print_3; +const print_2: ((ast: ASTNode) => string) & { + reset(): void; +}; // @public (undocumented) interface Printer { diff --git a/.api-reports/api-report-link_batch.md b/.api-reports/api-report-link_batch.md index 7562ad80181..a547973287d 100644 --- a/.api-reports/api-report-link_batch.md +++ b/.api-reports/api-report-link_batch.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_context.md b/.api-reports/api-report-link_context.md index bd2790f5381..af79db73e52 100644 --- a/.api-reports/api-report-link_context.md +++ b/.api-reports/api-report-link_context.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_core.md b/.api-reports/api-report-link_core.md index e711dfe7fc1..f488d284b51 100644 --- a/.api-reports/api-report-link_core.md +++ b/.api-reports/api-report-link_core.md @@ -23,10 +23,16 @@ export class ApolloLink { static execute(link: ApolloLink, operation: GraphQLRequest): Observable; // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // (undocumented) diff --git a/.api-reports/api-report-link_error.md b/.api-reports/api-report-link_error.md index febf5b6be00..af048d6fe6b 100644 --- a/.api-reports/api-report-link_error.md +++ b/.api-reports/api-report-link_error.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_http.md b/.api-reports/api-report-link_http.md index 363dff4a60e..fbd71df7348 100644 --- a/.api-reports/api-report-link_http.md +++ b/.api-reports/api-report-link_http.md @@ -11,7 +11,6 @@ import type { GraphQLError } from 'graphql'; import { InvariantError } from 'ts-invariant'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import { print as print_3 } from 'graphql'; // @public (undocumented) class ApolloLink { @@ -31,12 +30,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -249,7 +254,9 @@ export function parseAndCheckHttpResponse(operations: Operation | Operation[]): type Path = ReadonlyArray; // @public (undocumented) -const print_2: typeof print_3; +const print_2: ((ast: ASTNode) => string) & { + reset(): void; +}; // @public (undocumented) interface Printer { diff --git a/.api-reports/api-report-link_persisted-queries.md b/.api-reports/api-report-link_persisted-queries.md index dda0d3dd1db..14e7a0b47db 100644 --- a/.api-reports/api-report-link_persisted-queries.md +++ b/.api-reports/api-report-link_persisted-queries.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -57,7 +63,17 @@ interface BaseOptions { // Warning: (ae-forgotten-export) The symbol "ApolloLink" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const createPersistedQueryLink: (options: PersistedQueryLink.Options) => ApolloLink; +export const createPersistedQueryLink: (options: PersistedQueryLink.Options) => ApolloLink & { + resetHashCache: () => void; +} & ({ + getMemoryInternals(): { + PersistedQueryLink: { + persistedQueryHashes: number; + }; + }; +} | { + getMemoryInternals?: undefined; +}); // @public (undocumented) interface DefaultContext extends Record { diff --git a/.api-reports/api-report-link_remove-typename.md b/.api-reports/api-report-link_remove-typename.md index ddec205b0ac..f50798f5f02 100644 --- a/.api-reports/api-report-link_remove-typename.md +++ b/.api-reports/api-report-link_remove-typename.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -160,7 +166,15 @@ type Path = ReadonlyArray; // Warning: (ae-forgotten-export) The symbol "ApolloLink" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export function removeTypenameFromVariables(options?: RemoveTypenameFromVariablesOptions): ApolloLink; +export function removeTypenameFromVariables(options?: RemoveTypenameFromVariablesOptions): ApolloLink & ({ + getMemoryInternals(): { + removeTypenameFromVariables: { + getVariableDefinitions: number; + }; + }; +} | { + getMemoryInternals?: undefined; +}); // @public (undocumented) export interface RemoveTypenameFromVariablesOptions { diff --git a/.api-reports/api-report-link_retry.md b/.api-reports/api-report-link_retry.md index 1c3d0f2557d..a4a61a6ea1d 100644 --- a/.api-reports/api-report-link_retry.md +++ b/.api-reports/api-report-link_retry.md @@ -28,12 +28,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_schema.md b/.api-reports/api-report-link_schema.md index 31712ec362d..fcbee50828b 100644 --- a/.api-reports/api-report-link_schema.md +++ b/.api-reports/api-report-link_schema.md @@ -29,12 +29,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_subscriptions.md b/.api-reports/api-report-link_subscriptions.md index 5b5e1585f6e..8745a5772cb 100644 --- a/.api-reports/api-report-link_subscriptions.md +++ b/.api-reports/api-report-link_subscriptions.md @@ -29,12 +29,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-link_ws.md b/.api-reports/api-report-link_ws.md index 4ee65142d54..72a8165e4f0 100644 --- a/.api-reports/api-report-link_ws.md +++ b/.api-reports/api-report-link_ws.md @@ -30,12 +30,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index 851130e8f65..3489e1e54c3 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; import type { ExecutionResult } from 'graphql'; @@ -15,10 +13,10 @@ import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import * as React_2 from 'react'; -import { ReactNode } from 'react'; +import type * as ReactTypes from 'react'; 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'; import type { VariableDefinitionNode } from 'graphql'; @@ -43,6 +41,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -109,12 +111,16 @@ class ApolloClient implements DataProxy { 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 "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -172,12 +178,13 @@ interface ApolloClientOptions { 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; @@ -197,14 +204,14 @@ interface ApolloClientOptions { // Warning: (ae-forgotten-export) The symbol "ApolloConsumerProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const ApolloConsumer: React_2.FC; +export const ApolloConsumer: ReactTypes.FC; // @public (undocumented) interface ApolloConsumerProps { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // // (undocumented) - children: (client: ApolloClient) => React_2.ReactChild | null; + children: (client: ApolloClient) => ReactTypes.ReactChild | null; } // @public (undocumented) @@ -279,12 +286,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -298,25 +311,32 @@ class ApolloLink { // Warning: (ae-forgotten-export) The symbol "ApolloProviderProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const ApolloProvider: React_2.FC>; +export const ApolloProvider: ReactTypes.FC>; // @public (undocumented) interface ApolloProviderProps { // (undocumented) - children: React_2.ReactNode | React_2.ReactNode[] | null; + children: ReactTypes.ReactNode | ReactTypes.ReactNode[] | null; // (undocumented) client: ApolloClient; } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject = BackgroundQueryHookOptions, NoInfer>; +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export interface BaseMutationOptions = ApolloCache> extends Omit, "mutation"> { - // (undocumented) +export interface BaseMutationOptions = ApolloCache> extends MutationSharedOptions { client?: ApolloClient; - // (undocumented) ignoreResults?: boolean; - // (undocumented) notifyOnNetworkStatusChange?: boolean; - // (undocumented) onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; - // (undocumented) onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export interface BaseQueryOptions extends Omit, "query"> { - // (undocumented) +export interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: Context; - // (undocumented) ssr?: boolean; } // @public (undocumented) export interface BaseSubscriptionOptions { - // (undocumented) client?: ApolloClient; - // (undocumented) context?: Context; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchPolicy?: FetchPolicy; - // (undocumented) onComplete?: () => void; - // (undocumented) onData?: (options: OnDataOptions) => any; - // (undocumented) onError?: (error: ApolloError) => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionComplete?: () => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionData?: (options: OnSubscriptionDataOptions) => any; - // (undocumented) shouldResubscribe?: boolean | ((options: BaseSubscriptionOptions) => boolean); - // (undocumented) skip?: boolean; - // (undocumented) variables?: TVariables; } @@ -440,7 +447,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -492,13 +499,6 @@ namespace Cache_2 { import Fragment = DataProxy.Fragment; } -// @public (undocumented) -type CacheKey = [ -query: DocumentNode, -stringifiedVariables: string, -...queryKey: any[] -]; - // @public (undocumented) const enum CacheWriteBehavior { // (undocumented) @@ -547,6 +547,9 @@ type ConcastSourcesIterable = Iterable>; export interface Context extends Record { } +// @alpha +export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; + // @public (undocumented) namespace DataProxy { // (undocumented) @@ -573,6 +576,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -581,6 +585,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -682,14 +687,17 @@ class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -699,9 +707,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -776,9 +783,7 @@ type FetchMoreOptions = Parameters["fetchMore"]>[0 interface FetchMoreQueryOptions { // (undocumented) context?: Context; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -815,7 +820,57 @@ interface FragmentMap { type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; // @public (undocumented) -export function getApolloContext(): React_2.Context; +interface FulfilledPromise extends Promise { + // (undocumented) + status: "fulfilled"; + // (undocumented) + value: TValue; +} + +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + +// @public (undocumented) +export function getApolloContext(): ReactTypes.Context; // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -844,6 +899,15 @@ export interface IDocumentDefinition { variables: ReadonlyArray; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -863,34 +927,38 @@ 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): Promise>; + 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>; - // Warning: (ae-forgotten-export) The symbol "CacheKey" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "QueryKey" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly key: CacheKey; + 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-forgotten-export) The symbol "QueryRefPromise" needs to be exported by the entry point index.d.ts + // // (undocumented) - promise: Promise>; - // (undocumented) - promiseCache?: Map>>; + promise: QueryRefPromise; // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) + reinitialize(): void; + // (undocumented) result: ApolloQueryResult; // (undocumented) retain(): () => void; @@ -903,8 +971,6 @@ interface InternalQueryReferenceOptions { // (undocumented) autoDisposeTimeoutMs?: number; // (undocumented) - key: CacheKey; - // (undocumented) onDispose?: () => void; } @@ -965,17 +1031,48 @@ export interface LazyQueryHookExecOptions extends Omit, "skip"> { +export interface LazyQueryHookOptions extends BaseQueryOptions { + // @internal (undocumented) + defaultOptions?: Partial>; + onCompleted?: (data: TData) => void; + onError?: (error: ApolloError) => void; } // @public @deprecated (undocumented) export type LazyQueryResult = QueryResult; // @public (undocumented) -export type LazyQueryResultTuple = [LazyQueryExecFunction, QueryResult]; +export type LazyQueryResultTuple = [ +execute: LazyQueryExecFunction, +result: QueryResult +]; // @public (undocumented) -type Listener = (promise: Promise>) => void; +type Listener = (promise: QueryRefPromise) => void; + +// @public (undocumented) +export type LoadableQueryHookFetchPolicy = Extract; + +// @public (undocumented) +export interface LoadableQueryHookOptions { + // @deprecated + canonizeResults?: boolean; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + client?: ApolloClient; + context?: Context; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + errorPolicy?: ErrorPolicy; + fetchPolicy?: LoadableQueryHookFetchPolicy; + queryKey?: string | number | any[]; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type LoadQueryFunction = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; // @public (undocumented) class LocalState { @@ -1076,15 +1173,14 @@ type Modifiers = Record> = Partia 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); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => 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" @@ -1106,7 +1202,6 @@ export type MutationFunction = ApolloCache> extends BaseMutationOptions { - // (undocumented) mutation?: DocumentNode | TypedDocumentNode; } @@ -1114,14 +1209,8 @@ export interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { } -// 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; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1141,20 +1230,23 @@ type MutationQueryReducersMap { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data?: TData | null; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) reset(): void; } +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1169,8 +1261,8 @@ interface MutationStoreValue { // @public (undocumented) export type MutationTuple = ApolloCache> = [ -(options?: MutationFunctionOptions) => Promise>, -MutationResult +mutate: (options?: MutationFunctionOptions) => Promise>, +result: MutationResult ]; // @public (undocumented) @@ -1218,7 +1310,6 @@ class ObservableQuery; }); - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1250,6 +1341,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1265,22 +1358,31 @@ class ObservableQuery | 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) -export type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +export interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} // @public (undocumented) const OBSERVED_CHANGED_OPTIONS: readonly ["canonizeResults", "context", "errorPolicy", "fetchPolicy", "refetchWritePolicy", "returnPartialData"]; @@ -1298,6 +1400,11 @@ export interface OnDataOptions { data: SubscriptionResult; } +// @public +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1334,12 +1441,75 @@ type OperationVariables = Record; // @public (undocumented) export function parser(document: DocumentNode): IDocumentDefinition; +// @public (undocumented) +export namespace parser { + var // (undocumented) + resetCache: () => void; +} + // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +interface PendingPromise extends Promise { + // (undocumented) + status: "pending"; +} + +// @public (undocumented) +export type PreloadQueryFetchPolicy = Extract; + +// @public +export interface PreloadQueryFunction { + // Warning: (ae-forgotten-export) The symbol "PreloadQueryOptionsArg" needs to be exported by the entry point index.d.ts + >(query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg, TOptions>): QueryReference | 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; +} + +// 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; + 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; +// @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; @@ -1354,20 +1524,16 @@ interface QueryData { // @public (undocumented) export interface QueryDataOptions extends QueryFunctionOptions { // (undocumented) - children?: (result: QueryResult) => ReactNode; - // (undocumented) + children?: (result: QueryResult) => ReactTypes.ReactNode; query: DocumentNode | TypedDocumentNode; } // @public (undocumented) -export interface QueryFunctionOptions extends BaseQueryOptions { - // (undocumented) +export interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1405,7 +1571,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1419,6 +1585,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1432,11 +1600,15 @@ class QueryInfo { variables?: Record; } +// @public (undocumented) +interface QueryKey { + // (undocumented) + __queryKey?: string; +} + // @public @deprecated (undocumented) export interface QueryLazyOptions { - // (undocumented) context?: Context; - // (undocumented) variables?: TVariables; } @@ -1445,7 +1617,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1456,6 +1628,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1465,6 +1638,8 @@ class QueryManager { 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) @@ -1494,7 +1669,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1508,7 +1685,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1564,13 +1741,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated canonizeResults?: boolean; context?: Context; - // 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; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1581,30 +1758,31 @@ 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 { + // @internal (undocumented) + [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // - // (undocumented) - [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // @internal (undocumented) + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // @alpha + toPromise(): Promise>; } +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type QueryRefPromise = PromiseWithState>; + // @public (undocumented) export interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -1701,14 +1879,22 @@ type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +interface RejectedPromise extends Promise { + // (undocumented) + reason: unknown; + // (undocumented) + status: "rejected"; +} + // @public (undocumented) class RenderPromises { // (undocumented) - addObservableQueryPromise(obsQuery: ObservableQuery): ReactNode; + addObservableQueryPromise(obsQuery: ObservableQuery): ReactTypes.ReactNode; // Warning: (ae-forgotten-export) The symbol "QueryData" needs to be exported by the entry point index.d.ts // // (undocumented) - addQueryPromise(queryInstance: QueryData, finish?: () => React.ReactNode): React.ReactNode; + addQueryPromise(queryInstance: QueryData, finish?: () => ReactTypes.ReactNode): ReactTypes.ReactNode; // (undocumented) consumeAndAwaitPromises(): Promise; // (undocumented) @@ -1727,6 +1913,9 @@ type RequestHandler = (operation: Operation, forward: NextLink) => Observable void; + // @public (undocumented) type Resolver = (rootValue?: any, args?: any, context?: any, info?: { field: FieldNode; @@ -1758,6 +1947,27 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: Context; + errorPolicy?: ErrorPolicy; + 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; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = Context, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1831,7 +2041,6 @@ interface SubscriptionOptions { context?: Context; // 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; @@ -1839,13 +2048,10 @@ interface SubscriptionOptions { // @public (undocumented) export interface SubscriptionResult { - // (undocumented) data?: TData; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) + // @internal (undocumented) variables?: TVariables; } @@ -1853,13 +2059,20 @@ export interface SubscriptionResult { export type SuspenseQueryHookFetchPolicy = Extract; // @public (undocumented) -export interface SuspenseQueryHookOptions extends Pick, "client" | "variables" | "errorPolicy" | "context" | "canonizeResults" | "returnPartialData" | "refetchWritePolicy"> { - // (undocumented) +export interface SuspenseQueryHookOptions { + // @deprecated + canonizeResults?: boolean; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + client?: ApolloClient; + context?: Context; + errorPolicy?: ErrorPolicy; fetchPolicy?: SuspenseQueryHookFetchPolicy; - // (undocumented) queryKey?: string | number | any[]; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; // @deprecated skip?: boolean; + variables?: TVariables; } // @public (undocumented) @@ -1919,7 +2132,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 ]; @@ -1928,7 +2141,7 @@ export function useBackgroundQuery | undefined>, +QueryReference | undefined, TVariables>, UseBackgroundQueryResult ]; @@ -1936,7 +2149,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { errorPolicy: "ignore" | "all"; }): [ -QueryReference, +QueryReference, UseBackgroundQueryResult ]; @@ -1945,7 +2158,7 @@ export function useBackgroundQuery> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; @@ -1953,7 +2166,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; }): [ -QueryReference>, +QueryReference, TVariables>, UseBackgroundQueryResult ]; @@ -1961,12 +2174,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]; @@ -1975,13 +2191,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 ]; @@ -2013,18 +2229,60 @@ export type UseFragmentResult = { missing?: MissingTree; }; -// @public (undocumented) +// @public export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; // @public (undocumented) -export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions & TOptions): UseLoadableQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult | undefined, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; +}): UseLoadableQueryResult, TVariables>; + +// @public +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; // @public (undocumented) +export type UseLoadableQueryResult = [ +loadQuery: LoadQueryFunction, +queryRef: QueryReference | null, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; +} +]; + +// @public +export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; + +// @public export function useQuery(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; +// @public +export function useQueryRefHandlers(queryRef: QueryReference): UseQueryRefHandlersResult; + +// @public (undocumented) +export interface UseQueryRefHandlersResult { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; +} + // Warning: (ae-forgotten-export) The symbol "ReactiveVar" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export function useReactiveVar(rv: ReactiveVar): T; // @public (undocumented) @@ -2037,7 +2295,7 @@ export interface UseReadQueryResult { networkStatus: NetworkStatus; } -// @public (undocumented) +// @public export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer>): SubscriptionResult; // @public (undocumented) @@ -2101,54 +2359,46 @@ 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"; // @public -interface WatchQueryOptions { - canonizeResults?: boolean; - context?: Context; - // 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; +interface WatchQueryOptions extends SharedWatchQueryOptions { 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; - variables?: TVariables; } // 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/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104: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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380: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/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:26:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:27:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:121:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/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:106:1 - (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/.api-reports/api-report-react_components.md b/.api-reports/api-report-react_components.md index be5fa42249d..8307e63ec9c 100644 --- a/.api-reports/api-report-react_components.md +++ b/.api-reports/api-report-react_components.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; import type { ExecutionResult } from 'graphql'; @@ -16,8 +14,10 @@ import type { GraphQLErrorExtensions } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import * as PropTypes from 'prop-types'; +import type * as ReactTypes from 'react'; import type { Subscriber } from 'zen-observable-ts'; import type { Subscription as Subscription_2 } 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 @@ -41,6 +41,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -107,12 +111,16 @@ class ApolloClient implements DataProxy { 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 "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -171,12 +179,13 @@ interface ApolloClientOptions { 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; @@ -255,12 +264,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -272,14 +287,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject = ApolloCache> extends Omit, "mutation"> { +interface BaseMutationOptions = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts - // - // (undocumented) client?: ApolloClient; - // (undocumented) ignoreResults?: boolean; - // (undocumented) notifyOnNetworkStatusChange?: boolean; - // (undocumented) onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; - // (undocumented) onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface BaseQueryOptions extends Omit, "query"> { - // (undocumented) +interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } // @public (undocumented) interface BaseSubscriptionOptions { - // (undocumented) client?: ApolloClient; - // (undocumented) context?: DefaultContext; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchPolicy?: FetchPolicy; - // (undocumented) onComplete?: () => void; // Warning: (ae-forgotten-export) The symbol "OnDataOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) onData?: (options: OnDataOptions) => any; - // (undocumented) onError?: (error: ApolloError) => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionComplete?: () => void; // Warning: (ae-forgotten-export) The symbol "OnSubscriptionDataOptions" needs to be exported by the entry point index.d.ts // - // @deprecated (undocumented) + // @deprecated onSubscriptionData?: (options: OnSubscriptionDataOptions) => any; - // (undocumented) shouldResubscribe?: boolean | ((options: BaseSubscriptionOptions) => boolean); - // (undocumented) skip?: boolean; - // (undocumented) variables?: TVariables; } @@ -391,7 +398,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -508,6 +515,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -516,6 +524,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -587,14 +596,17 @@ class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -604,9 +616,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -657,9 +668,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -695,6 +704,48 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -712,6 +763,15 @@ interface GraphQLRequest> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -870,8 +930,8 @@ type Modifiers = Record> = Partia [FieldName in keyof T]: Modifier>>; }>; -// @public (undocumented) -export function Mutation(props: MutationComponentOptions): JSX.Element | null; +// @public @deprecated (undocumented) +export function Mutation(props: MutationComponentOptions): ReactTypes.JSX.Element | null; // @public (undocumented) export namespace Mutation { @@ -890,14 +950,14 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => 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" @@ -913,7 +973,7 @@ export interface MutationComponentOptions, result: MutationResult) => JSX.Element | null; + children: (mutateFunction: MutationFunction, result: MutationResult) => ReactTypes.JSX.Element | null; // (undocumented) mutation: DocumentNode | TypedDocumentNode; } @@ -928,18 +988,11 @@ type MutationFunction = ApolloCache> extends BaseMutationOptions { - // (undocumented) mutation?: DocumentNode | TypedDocumentNode; } -// 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; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -959,20 +1012,23 @@ type MutationQueryReducersMap { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data?: TData | null; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) reset(): void; } +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1030,8 +1086,6 @@ class ObservableQuery; }); // 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; @@ -1063,6 +1117,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1078,22 +1134,31 @@ class ObservableQuery | 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) -type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} // @public (undocumented) interface OnDataOptions { @@ -1138,8 +1203,8 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; -// @public (undocumented) -export function Query(props: QueryComponentOptions): JSX.Element | null; +// @public @deprecated (undocumented) +export function Query(props: QueryComponentOptions): ReactTypes.JSX.Element | null; // @public (undocumented) export namespace Query { @@ -1160,7 +1225,7 @@ export interface QueryComponentOptions) => JSX.Element | null; + children: (result: QueryResult) => ReactTypes.JSX.Element | null; // (undocumented) query: DocumentNode | TypedDocumentNode; } @@ -1168,14 +1233,11 @@ export interface QueryComponentOptions extends BaseQueryOptions { - // (undocumented) +interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1209,7 +1271,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1223,6 +1285,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1241,7 +1305,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1252,6 +1316,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1261,6 +1326,8 @@ class QueryManager { 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) @@ -1290,7 +1357,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1304,7 +1373,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1360,13 +1429,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated 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; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1378,21 +1447,13 @@ interface QueryOptions { // // @public (undocumented) interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -1497,6 +1558,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + 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; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1536,8 +1619,8 @@ type SubscribeToMoreOptions(props: SubscriptionComponentOptions): JSX.Element | null; +// @public @deprecated (undocumented) +export function Subscription(props: SubscriptionComponentOptions): ReactTypes.JSX.Element | null; // @public (undocumented) export namespace Subscription { @@ -1556,8 +1639,7 @@ export interface Subscription { // @public (undocumented) export interface SubscriptionComponentOptions extends BaseSubscriptionOptions { // (undocumented) - children?: null | ((result: SubscriptionResult) => JSX.Element | null); - // (undocumented) + children?: null | ((result: SubscriptionResult) => ReactTypes.JSX.Element | null); subscription: DocumentNode | TypedDocumentNode; } @@ -1566,7 +1648,6 @@ 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; @@ -1574,13 +1655,10 @@ interface SubscriptionOptions { // @public (undocumented) interface SubscriptionResult { - // (undocumented) data?: TData; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) + // @internal (undocumented) variables?: TVariables; } @@ -1638,48 +1716,28 @@ interface UriFunction { 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; +interface WatchQueryOptions extends SharedWatchQueryOptions { 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; - variables?: TVariables; } // 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/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104: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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380: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/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:121:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react_context.md b/.api-reports/api-report-react_context.md index 98b78cbbf82..337281bb134 100644 --- a/.api-reports/api-report-react_context.md +++ b/.api-reports/api-report-react_context.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; import type { ExecutionResult } from 'graphql'; @@ -15,10 +13,10 @@ import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import * as React_2 from 'react'; -import { ReactNode } from 'react'; +import type * as ReactTypes from 'react'; 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 @@ -42,6 +40,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -108,12 +110,16 @@ class ApolloClient implements DataProxy { 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 "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -172,12 +178,13 @@ interface ApolloClientOptions { 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; @@ -195,14 +202,14 @@ interface ApolloClientOptions { } // @public (undocumented) -export const ApolloConsumer: React_2.FC; +export const ApolloConsumer: ReactTypes.FC; // @public (undocumented) export interface ApolloConsumerProps { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts // // (undocumented) - children: (client: ApolloClient) => React_2.ReactChild | null; + children: (client: ApolloClient) => ReactTypes.ReactChild | null; } // @public (undocumented) @@ -277,12 +284,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -294,25 +307,32 @@ class ApolloLink { } // @public (undocumented) -export const ApolloProvider: React_2.FC>; +export const ApolloProvider: ReactTypes.FC>; // @public (undocumented) export interface ApolloProviderProps { // (undocumented) - children: React_2.ReactNode | React_2.ReactNode[] | null; + children: ReactTypes.ReactNode | ReactTypes.ReactNode[] | null; // (undocumented) client: ApolloClient; } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject extends Omit, "query"> { - // (undocumented) +interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } @@ -376,7 +396,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -493,6 +513,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -501,6 +522,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -572,14 +594,17 @@ class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -589,9 +614,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -642,9 +666,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -680,8 +702,50 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) -export function getApolloContext(): React_2.Context; +export function getApolloContext(): ReactTypes.Context; // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -700,6 +764,15 @@ interface GraphQLRequest> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -863,14 +936,14 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => 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" @@ -883,14 +956,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" 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; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -908,6 +977,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -965,8 +1043,6 @@ class ObservableQuery; }); // 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; @@ -998,6 +1074,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1013,22 +1091,31 @@ class ObservableQuery | 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) -type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1070,22 +1157,18 @@ interface QueryDataOptions) => ReactNode; - // (undocumented) + children?: (result: QueryResult) => ReactTypes.ReactNode; query: DocumentNode | TypedDocumentNode; } // Warning: (ae-forgotten-export) The symbol "BaseQueryOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface QueryFunctionOptions extends BaseQueryOptions { - // (undocumented) +interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1119,7 +1202,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1133,6 +1216,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1151,7 +1236,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1162,6 +1247,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1171,6 +1257,8 @@ class QueryManager { 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) @@ -1200,7 +1288,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1214,7 +1304,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1270,13 +1360,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated 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; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1288,21 +1378,13 @@ interface QueryOptions { // // @public (undocumented) interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -1376,11 +1458,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) class RenderPromises { // (undocumented) - addObservableQueryPromise(obsQuery: ObservableQuery): ReactNode; + addObservableQueryPromise(obsQuery: ObservableQuery): ReactTypes.ReactNode; // Warning: (ae-forgotten-export) The symbol "QueryData" needs to be exported by the entry point index.d.ts // // (undocumented) - addQueryPromise(queryInstance: QueryData, finish?: () => React.ReactNode): React.ReactNode; + addQueryPromise(queryInstance: QueryData, finish?: () => ReactTypes.ReactNode): ReactTypes.ReactNode; // (undocumented) consumeAndAwaitPromises(): Promise; // Warning: (ae-forgotten-export) The symbol "QueryDataOptions" needs to be exported by the entry point index.d.ts @@ -1432,6 +1514,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + 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; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1476,7 +1580,6 @@ 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; @@ -1536,48 +1639,28 @@ interface UriFunction { 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; +interface WatchQueryOptions extends SharedWatchQueryOptions { 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; - variables?: TVariables; } // 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/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104: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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380: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/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:121:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react_hoc.md b/.api-reports/api-report-react_hoc.md index a7312d0ca8e..2be6017e01e 100644 --- a/.api-reports/api-report-react_hoc.md +++ b/.api-reports/api-report-react_hoc.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; import type { ExecutionResult } from 'graphql'; @@ -15,9 +13,10 @@ import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import * as React_2 from 'react'; +import type * as ReactTypes from 'react'; 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 @@ -41,6 +40,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -107,12 +110,16 @@ class ApolloClient implements DataProxy { 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 "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -171,12 +178,13 @@ interface ApolloClientOptions { 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; @@ -255,12 +263,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -272,14 +286,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject = ApolloCache> extends Omit, "mutation"> { +interface BaseMutationOptions = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts - // - // (undocumented) client?: ApolloClient; - // (undocumented) ignoreResults?: boolean; - // (undocumented) notifyOnNetworkStatusChange?: boolean; - // (undocumented) onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; - // (undocumented) onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface BaseQueryOptions extends Omit, "query"> { - // (undocumented) +interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } @@ -359,7 +376,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -491,6 +508,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -499,6 +517,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -573,14 +592,17 @@ class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -590,9 +612,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -652,9 +673,7 @@ interface FetchMoreOptions { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -690,8 +709,50 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; -// @public (undocumented) -export function graphql> & Partial>>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: React.ComponentType) => React.ComponentClass; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + +// @public @deprecated (undocumented) +export function graphql> & Partial>>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -710,6 +771,15 @@ interface GraphQLRequest> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -885,14 +955,14 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => 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" @@ -914,18 +984,11 @@ type MutationFunction = ApolloCache> extends BaseMutationOptions { - // (undocumented) mutation?: DocumentNode | TypedDocumentNode; } -// 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; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -945,20 +1008,23 @@ type MutationQueryReducersMap { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data?: TData | null; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) reset(): void; } +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1016,8 +1082,6 @@ class ObservableQuery; }); // 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; @@ -1049,6 +1113,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1064,17 +1130,11 @@ class ObservableQuery | 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; } @@ -1187,7 +1247,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1201,6 +1261,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1219,7 +1281,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1230,6 +1292,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1239,6 +1302,8 @@ class QueryManager { 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) @@ -1268,7 +1333,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1282,7 +1349,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1338,13 +1405,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated 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; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1453,6 +1520,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + 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; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1497,7 +1586,6 @@ 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; @@ -1563,65 +1651,45 @@ interface UriFunction { 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; +interface WatchQueryOptions extends SharedWatchQueryOptions { 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; - variables?: TVariables; } -// @public (undocumented) -export function withApollo(WrappedComponent: React_2.ComponentType>>, operationOptions?: OperationOption): React_2.ComponentClass>; +// @public @deprecated (undocumented) +export function withApollo(WrappedComponent: ReactTypes.ComponentType>>, operationOptions?: OperationOption): ReactTypes.ComponentClass>; // @public (undocumented) export type WithApolloClient

= P & { client?: ApolloClient; }; -// @public (undocumented) -export function withMutation = {}, TGraphQLVariables extends OperationVariables = {}, TChildProps = MutateProps, TContext extends Record = DefaultContext, TCache extends ApolloCache = ApolloCache>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: React_2.ComponentType) => React_2.ComponentClass; +// @public @deprecated (undocumented) +export function withMutation = {}, TGraphQLVariables extends OperationVariables = {}, TChildProps = MutateProps, TContext extends Record = DefaultContext, TCache extends ApolloCache = ApolloCache>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; -// @public (undocumented) -export function withQuery = Record, TData extends object = {}, TGraphQLVariables extends object = {}, TChildProps extends object = DataProps>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: React_2.ComponentType) => React_2.ComponentClass; +// @public @deprecated (undocumented) +export function withQuery = Record, TData extends object = {}, TGraphQLVariables extends object = {}, TChildProps extends object = DataProps>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; -// @public (undocumented) -export function withSubscription>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: React_2.ComponentType) => React_2.ComponentClass; +// @public @deprecated (undocumented) +export function withSubscription>(document: DocumentNode, operationOptions?: OperationOption): (WrappedComponent: ReactTypes.ComponentType) => ReactTypes.ComponentClass; // 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/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104: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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380: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/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:121:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index 19b9bb03074..de2c0e84f6c 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -15,6 +15,7 @@ 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 @@ -38,6 +39,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -104,12 +109,16 @@ class ApolloClient implements DataProxy { 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 "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -168,12 +177,13 @@ interface ApolloClientOptions { 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; @@ -252,12 +262,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -269,14 +285,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject = BackgroundQueryHookOptions, NoInfer>; +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface BaseMutationOptions = ApolloCache> extends Omit, "mutation"> { +interface BaseMutationOptions = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts - // - // (undocumented) client?: ApolloClient; - // (undocumented) ignoreResults?: boolean; - // (undocumented) notifyOnNetworkStatusChange?: boolean; - // (undocumented) onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; - // (undocumented) onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface BaseQueryOptions extends Omit, "query"> { - // (undocumented) +interface BaseQueryOptions extends SharedWatchQueryOptions { + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } // @public (undocumented) interface BaseSubscriptionOptions { - // (undocumented) client?: ApolloClient; - // (undocumented) context?: DefaultContext; // Warning: (ae-forgotten-export) The symbol "FetchPolicy" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchPolicy?: FetchPolicy; - // (undocumented) onComplete?: () => void; // Warning: (ae-forgotten-export) The symbol "OnDataOptions" needs to be exported by the entry point index.d.ts - // - // (undocumented) onData?: (options: OnDataOptions) => any; - // (undocumented) onError?: (error: ApolloError) => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionComplete?: () => void; // Warning: (ae-forgotten-export) The symbol "OnSubscriptionDataOptions" needs to be exported by the entry point index.d.ts // - // @deprecated (undocumented) + // @deprecated onSubscriptionData?: (options: OnSubscriptionDataOptions) => any; - // (undocumented) shouldResubscribe?: boolean | ((options: BaseSubscriptionOptions) => boolean); - // (undocumented) skip?: boolean; - // (undocumented) variables?: TVariables; } @@ -413,7 +421,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -465,13 +473,6 @@ namespace Cache_2 { import Fragment = DataProxy.Fragment; } -// @public (undocumented) -type CacheKey = [ -query: DocumentNode, -stringifiedVariables: string, -...queryKey: any[] -]; - // @public (undocumented) const enum CacheWriteBehavior { // (undocumented) @@ -537,6 +538,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -545,6 +547,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -650,14 +653,17 @@ class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -667,9 +673,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -733,9 +738,7 @@ type FetchMoreOptions = Parameters["fetchMore"]>[0 interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -771,6 +774,56 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +interface FulfilledPromise extends Promise { + // (undocumented) + status: "fulfilled"; + // (undocumented) + value: TValue; +} + +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -788,6 +841,15 @@ interface GraphQLRequest> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -807,34 +869,38 @@ 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): Promise>; + 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>; - // Warning: (ae-forgotten-export) The symbol "CacheKey" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "QueryKey" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly key: CacheKey; + 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-forgotten-export) The symbol "QueryRefPromise" needs to be exported by the entry point index.d.ts + // // (undocumented) - promise: Promise>; - // (undocumented) - promiseCache?: Map>>; + promise: QueryRefPromise; // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) + reinitialize(): void; + // (undocumented) result: ApolloQueryResult; // (undocumented) retain(): () => void; @@ -847,8 +913,6 @@ interface InternalQueryReferenceOptions { // (undocumented) autoDisposeTimeoutMs?: number; // (undocumented) - key: CacheKey; - // (undocumented) onDispose?: () => void; } @@ -913,17 +977,51 @@ interface LazyQueryHookExecOptions; } +// Warning: (ae-forgotten-export) The symbol "BaseQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -interface LazyQueryHookOptions extends Omit, "skip"> { +interface LazyQueryHookOptions extends BaseQueryOptions { + // @internal (undocumented) + defaultOptions?: Partial>; + onCompleted?: (data: TData) => void; + onError?: (error: ApolloError) => void; } // Warning: (ae-forgotten-export) The symbol "LazyQueryExecFunction" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type LazyQueryResultTuple = [LazyQueryExecFunction, QueryResult]; +type LazyQueryResultTuple = [ +execute: LazyQueryExecFunction, +result: QueryResult +]; + +// @public (undocumented) +type Listener = (promise: QueryRefPromise) => void; // @public (undocumented) -type Listener = (promise: Promise>) => void; +type LoadableQueryHookFetchPolicy = Extract; + +// @public (undocumented) +interface LoadableQueryHookOptions { + // @deprecated + canonizeResults?: boolean; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + client?: ApolloClient; + context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "ErrorPolicy" needs to be exported by the entry point index.d.ts + errorPolicy?: ErrorPolicy; + // Warning: (ae-forgotten-export) The symbol "LoadableQueryHookFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: LoadableQueryHookFetchPolicy; + queryKey?: string | number | any[]; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; +} + +// Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type LoadQueryFunction = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; // @public (undocumented) class LocalState { @@ -1024,15 +1122,14 @@ type Modifiers = Record> = Partia 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); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => 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" @@ -1047,7 +1144,6 @@ type MutationFetchPolicy = Extract; // // @public (undocumented) interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { - // (undocumented) mutation?: DocumentNode | TypedDocumentNode; } @@ -1055,14 +1151,8 @@ interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { } -// 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; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1082,20 +1172,23 @@ type MutationQueryReducersMap { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data?: TData | null; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) reset(): void; } +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1113,8 +1206,8 @@ interface MutationStoreValue { // // @public (undocumented) type MutationTuple = ApolloCache> = [ -(options?: MutationFunctionOptions) => Promise>, -MutationResult +mutate: (options?: MutationFunctionOptions) => Promise>, +result: MutationResult ]; // @public (undocumented) @@ -1162,7 +1255,6 @@ class ObservableQuery; }); - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1194,6 +1286,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1209,22 +1303,31 @@ class ObservableQuery | 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) -type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} // @public (undocumented) const OBSERVED_CHANGED_OPTIONS: readonly ["canonizeResults", "context", "errorPolicy", "fetchPolicy", "refetchWritePolicy", "returnPartialData"]; @@ -1244,6 +1347,11 @@ interface OnDataOptions { data: SubscriptionResult; } +// @public +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1277,23 +1385,34 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +interface PendingPromise extends Promise { + // (undocumented) + status: "pending"; +} + // @public (undocumented) type Primitive = null | undefined | string | number | boolean | symbol | bigint; // @public (undocumented) -const QUERY_REFERENCE_SYMBOL: unique symbol; +const PROMISE_SYMBOL: unique symbol; -// Warning: (ae-forgotten-export) The symbol "BaseQueryOptions" needs to be exported by the entry point index.d.ts +// 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) -interface QueryFunctionOptions extends BaseQueryOptions { - // (undocumented) +type PromiseWithState = PendingPromise | FulfilledPromise | RejectedPromise; + +// @public (undocumented) +const QUERY_REFERENCE_SYMBOL: unique symbol; + +// @public (undocumented) +interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1333,7 +1452,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1347,6 +1466,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1360,12 +1481,18 @@ class QueryInfo { variables?: Record; } +// @public (undocumented) +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, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1376,6 +1503,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1385,6 +1513,8 @@ class QueryManager { 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) @@ -1414,7 +1544,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1428,7 +1560,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1484,13 +1616,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated 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; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1501,32 +1633,33 @@ 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 { + // @internal (undocumented) + [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // - // (undocumented) - [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // @internal (undocumented) + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // @alpha + toPromise(): Promise>; } +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type QueryRefPromise = PromiseWithState>; + // Warning: (ae-forgotten-export) The symbol "ObservableQueryFields" needs to be exported by the entry point index.d.ts // // @public (undocumented) interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -1617,9 +1750,20 @@ 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 ResetFunction = () => void; + // @public (undocumented) type Resolver = (rootValue?: any, args?: any, context?: any, info?: { field: FieldNode; @@ -1651,6 +1795,27 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + 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; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1710,7 +1875,6 @@ 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; @@ -1718,13 +1882,10 @@ interface SubscriptionOptions { // @public (undocumented) interface SubscriptionResult { - // (undocumented) data?: TData; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) + // @internal (undocumented) variables?: TVariables; } @@ -1732,15 +1893,21 @@ interface SubscriptionResult { type SuspenseQueryHookFetchPolicy = Extract; // @public (undocumented) -interface SuspenseQueryHookOptions extends Pick, "client" | "variables" | "errorPolicy" | "context" | "canonizeResults" | "returnPartialData" | "refetchWritePolicy"> { +interface SuspenseQueryHookOptions { + // @deprecated + canonizeResults?: boolean; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" + client?: ApolloClient; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; // Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookFetchPolicy" needs to be exported by the entry point index.d.ts - // - // (undocumented) fetchPolicy?: SuspenseQueryHookFetchPolicy; - // (undocumented) queryKey?: string | number | any[]; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; // @deprecated skip?: boolean; + variables?: TVariables; } // @public (undocumented) @@ -1801,7 +1968,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 ]; @@ -1810,7 +1977,7 @@ export function useBackgroundQuery | undefined>, +QueryReference | undefined, TVariables>, UseBackgroundQueryResult ]; @@ -1818,7 +1985,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { errorPolicy: "ignore" | "all"; }): [ -QueryReference, +QueryReference, UseBackgroundQueryResult ]; @@ -1827,7 +1994,7 @@ export function useBackgroundQuery> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; @@ -1835,7 +2002,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; }): [ -QueryReference>, +QueryReference, TVariables>, UseBackgroundQueryResult ]; @@ -1843,12 +2010,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]; @@ -1857,13 +2027,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 ]; @@ -1897,21 +2067,65 @@ export type UseFragmentResult = { // Warning: (ae-forgotten-export) The symbol "LazyQueryResultTuple" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; +// Warning: (ae-forgotten-export) The symbol "LoadableQueryHookOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions & TOptions): UseLoadableQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult | undefined, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; +}): UseLoadableQueryResult, TVariables>; + +// @public +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; + +// @public (undocumented) +export type UseLoadableQueryResult = [ +loadQuery: LoadQueryFunction, +queryRef: QueryReference | null, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; +} +]; + // Warning: (ae-forgotten-export) The symbol "MutationHookOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "MutationTuple" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; -// @public (undocumented) +// @public export function useQuery(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; +// @public +export function useQueryRefHandlers(queryRef: QueryReference): UseQueryRefHandlersResult; + +// @public (undocumented) +export interface UseQueryRefHandlersResult { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; +} + // Warning: (ae-forgotten-export) The symbol "ReactiveVar" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export function useReactiveVar(rv: ReactiveVar): T; // @public (undocumented) @@ -1926,7 +2140,7 @@ export interface UseReadQueryResult { // Warning: (ae-forgotten-export) The symbol "SubscriptionHookOptions" needs to be exported by the entry point index.d.ts // -// @public (undocumented) +// @public export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer>): SubscriptionResult; // Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookOptions" needs to be exported by the entry point index.d.ts @@ -1996,50 +2210,31 @@ export interface UseSuspenseQueryResult { - 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; +interface WatchQueryOptions extends SharedWatchQueryOptions { 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; - variables?: TVariables; } // 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/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104: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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380: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/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:26:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:27:3 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:121:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/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:106:1 - (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/.api-reports/api-report-react_internal.md b/.api-reports/api-report-react_internal.md new file mode 100644 index 00000000000..fb9449336cd --- /dev/null +++ b/.api-reports/api-report-react_internal.md @@ -0,0 +1,1699 @@ +## API Report File for "@apollo/client" + +> 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 "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; + // 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 "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; + // 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; + 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; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; + // (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) +interface ApolloQueryResult { + // (undocumented) + data: T; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts + error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) + loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) + networkStatus: NetworkStatus; + // (undocumented) + 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 { + // @deprecated (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 { + // @deprecated + 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 { + // @deprecated + 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) +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; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; + // (undocumented) + transformDocument(document: DocumentNode): DocumentNode; +} + +// @public (undocumented) +type DocumentTransformCacheKey = ReadonlyArray; + +// @public (undocumented) +interface DocumentTransformOptions { + cache?: boolean; + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts + 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; + query?: DocumentNode | TypedDocumentNode; + 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; +} + +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + +// 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 +// +// @public (undocumented) +export function getSuspenseCache(client: ApolloClient & { + [suspenseCacheSymbol]?: SuspenseCache; +}): SuspenseCache; + +// Warning: (ae-forgotten-export) The symbol "QueryRefPromise" needs to be exported by the entry point index.d.ts +// +// @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 IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + +// @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); + // (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; + // (undocumented) + promise: QueryRefPromise; + // (undocumented) + refetch(variables: OperationVariables | undefined): Promise>; + // (undocumented) + reinitialize(): void; + // (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; + +// @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 + 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, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); + refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; + // Warning: (ae-forgotten-export) The symbol "MutationUpdaterFunction" needs to be exported by the entry point index.d.ts + update?: MutationUpdaterFunction; + // 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 "MutationSharedOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationOptions = ApolloCache> extends MutationSharedOptions { + 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; +}; + +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + +// @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 + 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>; + // @internal (undocumented) + resetDiff(): void; + // (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; + startPolling(pollInterval: number): void; + stopPolling(): void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreOptions" needs to be exported by the entry point index.d.ts + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + 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) + resetDiff(): 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; + }): boolean; + // (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 { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: FetchPolicy; + notifyOnNetworkStatusChange?: boolean; + // @deprecated + 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 { + // @internal (undocumented) + [PROMISE_SYMBOL]: QueryRefPromise; + // @internal (undocumented) + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // @alpha + toPromise(): Promise>; +} + +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +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 SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + 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; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + +// @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; + 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; + +// @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"; + +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// +// @public +interface WatchQueryOptions extends SharedWatchQueryOptions { + query: DocumentNode | TypedDocumentNode; +} + +// @public (undocumented) +export function wrapQueryRef(internalQueryRef: InternalQueryReference): QueryReference; + +// Warnings were encountered during analysis: +// +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104: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:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts + +// (No @packageDocumentation comment for this package) + +``` diff --git a/.api-reports/api-report-react_parser.md b/.api-reports/api-report-react_parser.md index 96d961506af..9dd7eab4a8b 100644 --- a/.api-reports/api-report-react_parser.md +++ b/.api-reports/api-report-react_parser.md @@ -34,6 +34,12 @@ export function operationName(type: DocumentType_2): string; // @public (undocumented) export function parser(document: DocumentNode): IDocumentDefinition; +// @public (undocumented) +export namespace parser { + var // (undocumented) + resetCache: () => void; +} + // @public (undocumented) export function verifyDocumentType(document: DocumentNode, type: DocumentType_2): void; diff --git a/.api-reports/api-report-react_ssr.md b/.api-reports/api-report-react_ssr.md index 3e9fda62c0a..a6057e4a7eb 100644 --- a/.api-reports/api-report-react_ssr.md +++ b/.api-reports/api-report-react_ssr.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import type { DocumentNode } from 'graphql'; import type { ExecutionResult } from 'graphql'; @@ -15,11 +13,10 @@ import type { GraphQLError } from 'graphql'; import type { GraphQLErrorExtensions } from 'graphql'; import { Observable } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import * as React_2 from 'react'; -import type { ReactElement } from 'react'; -import { ReactNode } from 'react'; +import type * as ReactTypes from 'react'; 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 @@ -43,6 +40,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -109,12 +110,16 @@ class ApolloClient implements DataProxy { 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 "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -173,12 +178,13 @@ interface ApolloClientOptions { 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; @@ -257,12 +263,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -274,14 +286,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject extends Omit, "query"> { +interface BaseQueryOptions extends SharedWatchQueryOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts - // - // (undocumented) + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@apollo/client" does not have an export "ApolloClient" client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } @@ -347,7 +365,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -464,6 +482,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -472,6 +491,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -543,14 +563,17 @@ class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -560,9 +583,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -613,9 +635,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -651,8 +671,50 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) -export function getDataFromTree(tree: React_2.ReactNode, context?: { +export function getDataFromTree(tree: ReactTypes.ReactNode, context?: { [key: string]: any; }): Promise; @@ -663,11 +725,11 @@ export function getMarkupFromTree({ tree, context, renderFunction, }: GetMarkupF // @public (undocumented) type GetMarkupFromTreeOptions = { - tree: React_2.ReactNode; + tree: ReactTypes.ReactNode; context?: { [key: string]: any; }; - renderFunction?: (tree: React_2.ReactElement) => string | PromiseLike; + renderFunction?: (tree: ReactTypes.ReactElement) => string | PromiseLike; }; // @public (undocumented) @@ -687,6 +749,15 @@ interface GraphQLRequest> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -850,14 +921,14 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => 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" @@ -870,14 +941,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" 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; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -895,6 +962,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -952,8 +1028,6 @@ class ObservableQuery; }); // 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; @@ -985,6 +1059,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1000,22 +1076,31 @@ class ObservableQuery | 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) -type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1057,22 +1142,18 @@ interface QueryDataOptions) => ReactNode; - // (undocumented) + children?: (result: QueryResult) => ReactTypes.ReactNode; query: DocumentNode | TypedDocumentNode; } // Warning: (ae-forgotten-export) The symbol "BaseQueryOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) -interface QueryFunctionOptions extends BaseQueryOptions { - // (undocumented) +interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1106,7 +1187,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1120,6 +1201,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1138,7 +1221,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1149,6 +1232,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1158,6 +1242,8 @@ class QueryManager { 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) @@ -1187,7 +1273,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1201,7 +1289,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1257,13 +1345,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated 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; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1275,21 +1363,13 @@ interface QueryOptions { // // @public (undocumented) interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -1363,11 +1443,11 @@ type RefetchWritePolicy = "merge" | "overwrite"; // @public (undocumented) export class RenderPromises { // (undocumented) - addObservableQueryPromise(obsQuery: ObservableQuery): ReactNode; + addObservableQueryPromise(obsQuery: ObservableQuery): ReactTypes.ReactNode; // Warning: (ae-forgotten-export) The symbol "QueryData" needs to be exported by the entry point index.d.ts // // (undocumented) - addQueryPromise(queryInstance: QueryData, finish?: () => React.ReactNode): React.ReactNode; + addQueryPromise(queryInstance: QueryData, finish?: () => ReactTypes.ReactNode): ReactTypes.ReactNode; // (undocumented) consumeAndAwaitPromises(): Promise; // Warning: (ae-forgotten-export) The symbol "QueryDataOptions" needs to be exported by the entry point index.d.ts @@ -1383,7 +1463,7 @@ export class RenderPromises { } // @public (undocumented) -export function renderToStringWithData(component: ReactElement): Promise; +export function renderToStringWithData(component: ReactTypes.ReactElement): Promise; // @public (undocumented) type RequestHandler = (operation: Operation, forward: NextLink) => Observable | null; @@ -1419,6 +1499,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + 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; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1463,7 +1565,6 @@ 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; @@ -1523,48 +1624,28 @@ interface UriFunction { 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; +interface WatchQueryOptions extends SharedWatchQueryOptions { 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; - variables?: TVariables; } // 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/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104: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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380: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/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:121:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index e0a1de0791d..5b4ae8e658b 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -16,6 +16,7 @@ import type { Observer } from 'zen-observable-ts'; import * as React_2 from 'react'; 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 @@ -39,6 +40,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -105,12 +110,16 @@ class ApolloClient implements DataProxy { 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 "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -169,12 +178,13 @@ interface ApolloClientOptions { 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; @@ -253,12 +263,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -270,14 +286,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -454,6 +477,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -462,6 +486,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -533,14 +558,17 @@ class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -550,9 +578,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -603,9 +630,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -641,6 +666,48 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -658,6 +725,15 @@ interface GraphQLRequest> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -835,7 +911,7 @@ export interface MockedProviderProps { // (undocumented) link?: ApolloLink; // (undocumented) - mocks?: ReadonlyArray; + mocks?: ReadonlyArray>; // (undocumented) resolvers?: Resolvers; // (undocumented) @@ -855,11 +931,17 @@ export interface MockedResponse, TVariables = Record // (undocumented) error?: Error; // (undocumented) + maxUsageCount?: number; + // (undocumented) newData?: ResultFunction; // (undocumented) request: GraphQLRequest; // (undocumented) - result?: FetchResult | ResultFunction>; + result?: FetchResult | ResultFunction, TVariables>; + // Warning: (ae-forgotten-export) The symbol "VariableMatcher" needs to be exported by the entry point index.d.ts + // + // (undocumented) + variableMatcher?: VariableMatcher; } // @public (undocumented) @@ -874,7 +956,7 @@ interface MockedSubscriptionResult { // @public (undocumented) export class MockLink extends ApolloLink { - constructor(mockedResponses: ReadonlyArray, addTypename?: Boolean, options?: MockLinkOptions); + constructor(mockedResponses: ReadonlyArray>, addTypename?: Boolean, options?: MockLinkOptions); // (undocumented) addMockedResponse(mockedResponse: MockedResponse): void; // (undocumented) @@ -954,14 +1036,14 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => 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" @@ -974,14 +1056,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" 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; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -999,6 +1077,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1066,8 +1153,6 @@ class ObservableQuery; }); // 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; @@ -1099,6 +1184,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1114,17 +1201,11 @@ class ObservableQuery | 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; } @@ -1183,7 +1264,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1197,6 +1278,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1215,7 +1298,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1226,6 +1309,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1236,6 +1320,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly defaultContext: Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) readonly documentTransform: DocumentTransform; @@ -1262,7 +1348,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1276,7 +1364,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1332,13 +1420,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated 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; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1431,7 +1519,7 @@ interface Resolvers { } // @public (undocumented) -export type ResultFunction = () => T; +export type ResultFunction> = (variables: V) => T; // @public (undocumented) type SafeReadonly = T extends object ? Readonly : T; @@ -1450,6 +1538,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + 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; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1497,7 +1607,6 @@ 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; @@ -1556,33 +1665,20 @@ interface UriFunction { (operation: Operation): string; } +// @public (undocumented) +type VariableMatcher> = (variables: V) => boolean; + // @public (undocumented) export function wait(ms: number): Promise; // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @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; +interface WatchQueryOptions extends SharedWatchQueryOptions { 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; - variables?: TVariables; } // @public @deprecated (undocumented) @@ -1596,24 +1692,22 @@ export function withWarningSpy(it: (...args: TArgs // 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/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104: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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380: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/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:121:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 71f9db61e0c..202108f79c7 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -15,6 +15,7 @@ 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 @@ -38,6 +39,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // Warning: (ae-forgotten-export) The symbol "StoreObject" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -104,12 +109,16 @@ class ApolloClient implements DataProxy { 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 "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -168,12 +177,13 @@ interface ApolloClientOptions { 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; @@ -252,12 +262,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -269,14 +285,21 @@ class ApolloLink { } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public type AsStoreObject extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -453,6 +476,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -461,6 +485,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -532,14 +557,17 @@ class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -549,9 +577,8 @@ type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) + // Warning: (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -602,9 +629,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -640,6 +665,48 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) type GraphQLErrors = ReadonlyArray; @@ -657,6 +724,15 @@ interface GraphQLRequest> { variables?: TVariables; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) interface IncrementalPayload { // (undocumented) @@ -810,11 +886,17 @@ export interface MockedResponse, TVariables = Record // (undocumented) error?: Error; // (undocumented) + maxUsageCount?: number; + // (undocumented) newData?: ResultFunction; // (undocumented) request: GraphQLRequest; // (undocumented) - result?: FetchResult | ResultFunction>; + result?: FetchResult | ResultFunction, TVariables>; + // Warning: (ae-forgotten-export) The symbol "VariableMatcher" needs to be exported by the entry point index.d.ts + // + // (undocumented) + variableMatcher?: VariableMatcher; } // @public (undocumented) @@ -829,7 +911,7 @@ interface MockedSubscriptionResult { // @public (undocumented) export class MockLink extends ApolloLink { - constructor(mockedResponses: ReadonlyArray, addTypename?: Boolean, options?: MockLinkOptions); + constructor(mockedResponses: ReadonlyArray>, addTypename?: Boolean, options?: MockLinkOptions); // (undocumented) addMockedResponse(mockedResponse: MockedResponse): void; // (undocumented) @@ -909,14 +991,14 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => 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" @@ -929,14 +1011,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" 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; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -954,6 +1032,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1021,8 +1108,6 @@ class ObservableQuery; }); // 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; @@ -1054,6 +1139,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1069,17 +1156,11 @@ class ObservableQuery | 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; } @@ -1138,7 +1219,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1152,6 +1233,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1170,7 +1253,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1181,6 +1264,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1190,6 +1274,8 @@ class QueryManager { 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) @@ -1219,7 +1305,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1233,7 +1321,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1289,13 +1377,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated 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; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -1388,7 +1476,7 @@ interface Resolvers { } // @public (undocumented) -export type ResultFunction = () => T; +export type ResultFunction> = (variables: V) => T; // @public (undocumented) type SafeReadonly = T extends object ? Readonly : T; @@ -1407,6 +1495,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + 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; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -1454,7 +1564,6 @@ 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; @@ -1513,33 +1622,20 @@ interface UriFunction { (operation: Operation): string; } +// @public (undocumented) +type VariableMatcher> = (variables: V) => boolean; + // @public (undocumented) export function wait(ms: number): Promise; // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @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; +interface WatchQueryOptions extends SharedWatchQueryOptions { 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; - variables?: TVariables; } // @public @deprecated (undocumented) @@ -1553,24 +1649,22 @@ export function withWarningSpy(it: (...args: TArgs // 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/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:100:3 - (ae-forgotten-export) The symbol "ReadFieldFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:101:3 - (ae-forgotten-export) The symbol "CanReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:102:3 - (ae-forgotten-export) The symbol "isReference" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:103:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/common.ts:104: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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380: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/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/DocumentTransform.ts:121:7 - (ae-forgotten-export) The symbol "DocumentTransformCacheKey" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-utilities.md b/.api-reports/api-report-utilities.md index 46a9d71f9dd..715d0827a56 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -20,15 +20,16 @@ import { Observable } from 'zen-observable-ts'; import type { Subscription as ObservableSubscription } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; import type { OperationDefinitionNode } from 'graphql'; -import { print as print_3 } from 'graphql'; import type { SelectionNode } from 'graphql'; import type { SelectionSetNode } from 'graphql'; +import { StrongCache } from '@wry/caches'; import type { Subscriber } from 'zen-observable-ts'; import { Trie } from '@wry/trie'; import { TypedDocumentNode } from '@graphql-typed-document-node/core'; import type { ValueNode } from 'graphql'; import type { VariableDefinitionNode } from 'graphql'; import type { VariableNode } from 'graphql'; +import { WeakCache } from '@wry/caches'; // @public (undocumented) export const addTypenameToDocument: ((doc: TNode) => TNode) & { @@ -56,6 +57,10 @@ abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -118,11 +123,15 @@ class ApolloClient implements DataProxy { cache: ApolloCache; clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; // Warning: (ae-forgotten-export) The symbol "RefetchQueriesInclude" needs to be exported by the entry point index.d.ts getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; @@ -181,12 +190,13 @@ interface ApolloClientOptions { 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; @@ -265,12 +275,18 @@ class ApolloLink { // // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: 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; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // Warning: (ae-forgotten-export) The symbol "Operation" needs to be exported by the entry point index.d.ts @@ -293,14 +309,21 @@ interface ApolloPayloadResult, TExtensions = Record< } // @public (undocumented) -type ApolloQueryResult = { +interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; + // Warning: (ae-forgotten-export) The symbol "ApolloError" needs to be exported by the entry point index.d.ts error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // Warning: (ae-forgotten-export) The symbol "NetworkStatus" needs to be exported by the entry point index.d.ts + // + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public (undocumented) type ApolloReducerConfig = { @@ -321,6 +344,18 @@ export type AsStoreObject(observable: Observable, mapFn: (value: V) => R | PromiseLike, catchFn?: (error: any) => R | PromiseLike): Observable; +// @internal +export const AutoCleanedStrongCache: typeof StrongCache; + +// @internal (undocumented) +export type AutoCleanedStrongCache = StrongCache; + +// @internal +export const AutoCleanedWeakCache: typeof WeakCache; + +// @internal (undocumented) +export type AutoCleanedWeakCache = WeakCache; + // Warning: (ae-forgotten-export) The symbol "InMemoryCache" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -374,7 +409,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -441,6 +476,27 @@ class CacheGroup { resetCaching(): void; } +// @public +export interface CacheSizes { + "cache.fragmentQueryDocuments": number; + "documentTransform.cache": number; + "fragmentRegistry.findFragmentSpreads": number; + "fragmentRegistry.lookup": number; + "fragmentRegistry.transform": number; + "inMemoryCache.executeSelectionSet": number; + "inMemoryCache.executeSubSelectedArray": number; + "inMemoryCache.maybeBroadcastWatch": number; + "PersistedQueryLink.persistedQueryHashes": number; + "queryManager.getDocumentInfo": number; + "removeTypenameFromVariables.getVariableDefinitions": number; + canonicalStringify: number; + parser: number; + print: number; +} + +// @public +export const cacheSizes: Partial; + // @public (undocumented) const enum CacheWriteBehavior { // (undocumented) @@ -451,6 +507,11 @@ const enum CacheWriteBehavior { OVERWRITE = 1 } +// @public +export const canonicalStringify: ((value: any) => string) & { + reset(): void; +}; + // @public (undocumented) type CanReadFunction = (value: StoreValue) => boolean; @@ -552,6 +613,7 @@ namespace DataProxy { // // (undocumented) interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -560,6 +622,7 @@ namespace DataProxy { // // (undocumented) interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -659,6 +722,38 @@ type DeepPartialReadonlySet = {} & ReadonlySet>; // @public (undocumented) type DeepPartialSet = {} & Set>; +// @public (undocumented) +export const enum defaultCacheSizes { + // (undocumented) + "cache.fragmentQueryDocuments" = 1000, + // (undocumented) + "documentTransform.cache" = 2000, + // (undocumented) + "fragmentRegistry.findFragmentSpreads" = 4000, + // (undocumented) + "fragmentRegistry.lookup" = 1000, + // (undocumented) + "fragmentRegistry.transform" = 2000, + // (undocumented) + "inMemoryCache.executeSelectionSet" = 50000, + // (undocumented) + "inMemoryCache.executeSubSelectedArray" = 10000, + // (undocumented) + "inMemoryCache.maybeBroadcastWatch" = 5000, + // (undocumented) + "PersistedQueryLink.persistedQueryHashes" = 2000, + // (undocumented) + "queryManager.getDocumentInfo" = 2000, + // (undocumented) + "removeTypenameFromVariables.getVariableDefinitions" = 2000, + // (undocumented) + canonicalStringify = 1000, + // (undocumented) + parser = 1000, + // (undocumented) + print = 2000 +} + // @public (undocumented) interface DefaultContext extends Record { } @@ -707,14 +802,17 @@ export class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -724,9 +822,7 @@ export type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -779,7 +875,10 @@ abstract class EntityStore implements NormalizedCache { has(dataId: string): boolean; // (undocumented) protected lookup(dataId: string, dependOnExistence?: boolean): StoreObject | undefined; - // (undocumented) + makeCacheKey(document: DocumentNode, callback: Cache_2.WatchCallback, details: string): object; + makeCacheKey(selectionSet: SelectionSetNode, parent: string | StoreObject, varString: string | undefined, canonizeResults: boolean): object; + makeCacheKey(field: FieldNode, array: readonly any[], varString: string | undefined): object; + // @deprecated (undocumented) makeCacheKey(...args: any[]): object; // (undocumented) merge(older: string | StoreObject, newer: StoreObject | string): void; @@ -876,9 +975,7 @@ interface ExecutionPatchResultBase { interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -981,6 +1078,8 @@ interface FragmentRegistryAPI { // (undocumented) register(...fragments: DocumentNode[]): this; // (undocumented) + resetCaches(): void; + // (undocumented) transform(document: D): D; } @@ -992,6 +1091,48 @@ interface FulfilledPromise extends Promise { value: TValue; } +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + // @public (undocumented) export function getDefaultValues(definition: OperationDefinitionNode | undefined): Record; @@ -1022,6 +1163,26 @@ export function getGraphQLErrorsFromResult(result: FetchResult): GraphQLEr // @public (undocumented) export function getInclusionDirectives(directives: ReadonlyArray): InclusionDirectives; +// @internal +const getInMemoryCacheMemoryInternals: (() => { + addTypenameDocumentTransform: { + cache: number; + }[]; + inMemoryCache: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + }; + fragmentRegistry: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + }; + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + // @public export function getMainDefinition(queryDoc: DocumentNode): OperationDefinitionNode | FragmentDefinitionNode; @@ -1042,7 +1203,7 @@ export function getQueryDefinition(doc: DocumentNode): OperationDefinitionNode; // @public (undocumented) export const getStoreKeyName: ((fieldName: string, args?: Record | null, directives?: Directives) => string) & { - setStringify(s: typeof stringify): (value: any) => string; + setStringify(s: typeof storeKeyNameStringify): (value: any) => string; }; // @public (undocumented) @@ -1095,6 +1256,15 @@ interface IdGetterObj extends Object { _id?: string; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) export type InclusionDirectives = Array<{ directive: DirectiveNode; @@ -1143,6 +1313,10 @@ class InMemoryCache extends ApolloCache { resetResultCache?: boolean; resetResultIdentities?: boolean; }): string[]; + // Warning: (ae-forgotten-export) The symbol "getInMemoryCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // Warning: (ae-forgotten-export) The symbol "makeVar" needs to be exported by the entry point index.d.ts @@ -1179,7 +1353,7 @@ class InMemoryCache extends ApolloCache { // // @public (undocumented) interface InMemoryCacheConfig extends ApolloReducerConfig { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // Warning: (ae-forgotten-export) The symbol "FragmentRegistryAPI" needs to be exported by the entry point index.d.ts // @@ -1189,7 +1363,7 @@ interface InMemoryCacheConfig extends ApolloReducerConfig { // // (undocumented) possibleTypes?: PossibleTypesMap; - // (undocumented) + // @deprecated (undocumented) resultCacheMaxSize?: number; // (undocumented) resultCaching?: boolean; @@ -1281,8 +1455,6 @@ export function isQueryOperation(document: DocumentNode): boolean; // @public (undocumented) export function isReference(obj: any): obj is Reference; -// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export function isStatefulPromise(promise: Promise): promise is PromiseWithState; @@ -1506,14 +1678,14 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => 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" @@ -1526,14 +1698,10 @@ interface MutationBaseOptions; -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" 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; +interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1551,6 +1719,15 @@ type MutationQueryReducersMap; }; +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + // Warning: (ae-forgotten-export) The symbol "MutationFetchPolicy" needs to be exported by the entry point index.d.ts + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1653,8 +1830,6 @@ class ObservableQuery; }); // 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; @@ -1684,6 +1859,8 @@ class ObservableQuery>, newNetworkStatus?: NetworkStatus): Promise>; // (undocumented) reobserveAsConcast(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1699,17 +1876,11 @@ class ObservableQuery | 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; } @@ -1723,6 +1894,11 @@ export function offsetLimitPagination(keyArgs?: KeyArgs): FieldPo // @public (undocumented) export function omitDeep(value: T, key: K): DeepOmit; +// @public +export type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1809,13 +1985,15 @@ type PossibleTypesMap = { type Primitive = null | undefined | string | number | boolean | symbol | bigint; // @public (undocumented) -const print_2: typeof print_3; +const print_2: ((ast: ASTNode) => string) & { + reset(): void; +}; export { print_2 as print } // Warning: (ae-forgotten-export) The symbol "PendingPromise" needs to be exported by the entry point index.d.ts // // @public (undocumented) -type PromiseWithState = PendingPromise | FulfilledPromise | RejectedPromise; +export type PromiseWithState = PendingPromise | FulfilledPromise | RejectedPromise; // @public (undocumented) class QueryInfo { @@ -1847,7 +2025,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1861,6 +2039,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1879,7 +2059,7 @@ type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1890,6 +2070,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -1899,6 +2080,8 @@ class QueryManager { 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) @@ -1928,7 +2111,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -1942,7 +2127,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -1998,13 +2183,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated 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; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -2195,6 +2380,28 @@ type ServerParseError = Error & { bodyText: string; }; +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + 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; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + // Warning: (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) export function shouldInclude({ directives }: SelectionNode, variables?: Record): boolean; @@ -2215,6 +2422,9 @@ type StorageType = Record; // @public (undocumented) export function storeKeyNameFromField(field: FieldNode, variables?: Object): string; +// @public (undocumented) +let storeKeyNameStringify: (value: any) => string; + // @public (undocumented) export interface StoreObject { // (undocumented) @@ -2229,9 +2439,6 @@ type StoreObjectValueMaybeReference = StoreVal extends Array string; - // @public (undocumented) export function stringifyForDisplay(value: any, space?: number): string; @@ -2261,7 +2468,6 @@ 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; @@ -2374,27 +2580,11 @@ export type VariableValue = (node: VariableNode) => any; // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @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; +interface WatchQueryOptions extends SharedWatchQueryOptions { 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; - variables?: TVariables; } // @public (undocumented) @@ -2432,26 +2622,25 @@ interface WriteContext extends ReadMergeModifyContext { // 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/inmemory/policies.ts:63:3 - (ae-forgotten-export) The symbol "TypePolicy" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:168:3 - (ae-forgotten-export) The symbol "FieldReadFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:169:3 - (ae-forgotten-export) The symbol "FieldMergeFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts +// src/cache/core/types/DataProxy.ts:146:7 - (ae-forgotten-export) The symbol "MissingFieldError" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:57:3 - (ae-forgotten-export) The symbol "TypePolicy" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "FieldReadFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:163:3 - (ae-forgotten-export) The symbol "FieldMergeFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/writeToStore.ts:65:7 - (ae-forgotten-export) The symbol "MergeTree" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts: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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380: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/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts // src/core/types.ts:174:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/utilities/graphql/storeUtils.ts:220:12 - (ae-forgotten-export) The symbol "stringify" needs to be exported by the entry point index.d.ts +// src/core/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/utilities/graphql/storeUtils.ts:226:12 - (ae-forgotten-export) The symbol "storeKeyNameStringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report-utilities_subscriptions_relay.md b/.api-reports/api-report-utilities_subscriptions_relay.md new file mode 100644 index 00000000000..4e77625a6e1 --- /dev/null +++ b/.api-reports/api-report-utilities_subscriptions_relay.md @@ -0,0 +1,28 @@ +## API Report File for "@apollo/client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { GraphQLResponse } from 'relay-runtime'; +import { Observable } from 'relay-runtime'; +import type { RequestParameters } from 'relay-runtime'; + +// Warning: (ae-forgotten-export) The symbol "CreateMultipartSubscriptionOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function createFetchMultipartSubscription(uri: string, { fetch: preferredFetch, headers }?: CreateMultipartSubscriptionOptions): (operation: RequestParameters, variables: OperationVariables) => Observable; + +// @public (undocumented) +type CreateMultipartSubscriptionOptions = { + fetch?: WindowOrWorkerGlobalScope["fetch"]; + headers?: Record; +}; + +// @public (undocumented) +type OperationVariables = Record; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/.api-reports/api-report-utilities_subscriptions_urql.md b/.api-reports/api-report-utilities_subscriptions_urql.md new file mode 100644 index 00000000000..833fe4492b5 --- /dev/null +++ b/.api-reports/api-report-utilities_subscriptions_urql.md @@ -0,0 +1,25 @@ +## API Report File for "@apollo/client" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { Observable } from 'zen-observable-ts'; + +// Warning: (ae-forgotten-export) The symbol "CreateMultipartSubscriptionOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export function createFetchMultipartSubscription(uri: string, { fetch: preferredFetch, headers }?: CreateMultipartSubscriptionOptions): ({ query, variables, }: { + query?: string | undefined; + variables: undefined | Record; +}) => Observable; + +// @public (undocumented) +type CreateMultipartSubscriptionOptions = { + fetch?: WindowOrWorkerGlobalScope["fetch"]; + headers?: Record; +}; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index d1024c01738..9040bd40123 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -4,8 +4,6 @@ ```ts -/// - import type { ASTNode } from 'graphql'; import { disableExperimentalFragmentVariables } from 'graphql-tag'; import { disableFragmentWarnings } from 'graphql-tag'; @@ -22,9 +20,7 @@ import { InvariantError } from 'ts-invariant'; import { Observable } from 'zen-observable-ts'; import type { Subscription as ObservableSubscription } from 'zen-observable-ts'; import type { Observer } from 'zen-observable-ts'; -import { print as print_3 } from 'graphql'; -import * as React_2 from 'react'; -import { ReactNode } from 'react'; +import type * as ReactTypes from 'react'; import { resetCaches } from 'graphql-tag'; import type { SelectionSetNode } from 'graphql'; import { setVerbosity as setLogVerbosity } from 'ts-invariant'; @@ -52,6 +48,10 @@ export abstract class ApolloCache implements DataProxy { abstract extract(optimistic?: boolean): TSerialized; // (undocumented) gc(): string[]; + // Warning: (ae-forgotten-export) The symbol "getApolloCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getApolloCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -103,11 +103,15 @@ export class ApolloClient implements DataProxy { cache: ApolloCache; clearStore(): Promise; // (undocumented) + get defaultContext(): Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) disableNetworkFetches: boolean; get documentTransform(): DocumentTransform; extract(optimistic?: boolean): TCacheShape; + // Warning: (ae-forgotten-export) The symbol "getApolloClientMemoryInternals" needs to be exported by the entry point index.d.ts + getMemoryInternals?: typeof getApolloClientMemoryInternals; getObservableQueries(include?: RefetchQueriesInclude): Map>; getResolvers(): Resolvers; // (undocumented) @@ -146,12 +150,13 @@ export interface ApolloClientOptions { connectToDevTools?: boolean; // (undocumented) credentials?: string; + // (undocumented) + defaultContext?: Partial; defaultOptions?: DefaultOptions; // (undocumented) documentTransform?: DocumentTransform; // (undocumented) fragmentMatcher?: FragmentMatcher; - // (undocumented) headers?: Record; link?: ApolloLink; name?: string; @@ -169,12 +174,12 @@ export interface ApolloClientOptions { // Warning: (ae-forgotten-export) The symbol "ApolloConsumerProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const ApolloConsumer: React_2.FC; +export const ApolloConsumer: ReactTypes.FC; // @public (undocumented) interface ApolloConsumerProps { // (undocumented) - children: (client: ApolloClient) => React_2.ReactChild | null; + children: (client: ApolloClient) => ReactTypes.ReactChild | null; } // @public (undocumented) @@ -244,10 +249,16 @@ export class ApolloLink { static execute(link: ApolloLink, operation: GraphQLRequest): Observable; // (undocumented) static from(links: (ApolloLink | RequestHandler)[]): ApolloLink; + // @internal + getMemoryInternals?: () => unknown; + // @internal + readonly left?: ApolloLink; // (undocumented) protected onError(error: any, observer?: Observer): false | void; // (undocumented) request(operation: Operation, forward?: NextLink): Observable | null; + // @internal + readonly right?: ApolloLink; // (undocumented) setOnError(fn: ApolloLink["onError"]): this; // (undocumented) @@ -267,25 +278,29 @@ export interface ApolloPayloadResult, TExtensions = // Warning: (ae-forgotten-export) The symbol "ApolloProviderProps" needs to be exported by the entry point index.d.ts // // @public (undocumented) -export const ApolloProvider: React_2.FC>; +export const ApolloProvider: ReactTypes.FC>; // @public (undocumented) interface ApolloProviderProps { // (undocumented) - children: React_2.ReactNode | React_2.ReactNode[] | null; + children: ReactTypes.ReactNode | ReactTypes.ReactNode[] | null; // (undocumented) client: ApolloClient; } // @public (undocumented) -export type ApolloQueryResult = { +export interface ApolloQueryResult { + // (undocumented) data: T; - errors?: ReadonlyArray; error?: ApolloError; + errors?: ReadonlyArray; + // (undocumented) loading: boolean; + // (undocumented) networkStatus: NetworkStatus; + // (undocumented) partial?: boolean; -}; +} // @public (undocumented) export type ApolloReducerConfig = { @@ -316,53 +331,40 @@ export interface BackgroundQueryHookOptions = BackgroundQueryHookOptions, NoInfer>; +// Warning: (ae-forgotten-export) The symbol "MutationSharedOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export interface BaseMutationOptions = ApolloCache> extends Omit, "mutation"> { - // (undocumented) +export interface BaseMutationOptions = ApolloCache> extends MutationSharedOptions { client?: ApolloClient; - // (undocumented) ignoreResults?: boolean; - // (undocumented) notifyOnNetworkStatusChange?: boolean; - // (undocumented) onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; - // (undocumented) onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; } +// Warning: (ae-forgotten-export) The symbol "SharedWatchQueryOptions" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -export interface BaseQueryOptions extends Omit, "query"> { - // (undocumented) +export interface BaseQueryOptions extends SharedWatchQueryOptions { client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) ssr?: boolean; } // @public (undocumented) export interface BaseSubscriptionOptions { - // (undocumented) client?: ApolloClient; - // (undocumented) context?: DefaultContext; - // (undocumented) fetchPolicy?: FetchPolicy; - // (undocumented) onComplete?: () => void; - // (undocumented) onData?: (options: OnDataOptions) => any; - // (undocumented) onError?: (error: ApolloError) => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionComplete?: () => void; - // @deprecated (undocumented) + // @deprecated onSubscriptionData?: (options: OnSubscriptionDataOptions) => any; - // (undocumented) shouldResubscribe?: boolean | ((options: BaseSubscriptionOptions) => boolean); - // (undocumented) skip?: boolean; - // (undocumented) variables?: TVariables; } @@ -424,7 +426,7 @@ namespace Cache_2 { } // (undocumented) interface ReadOptions extends DataProxy.Query { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // (undocumented) optimistic: boolean; @@ -486,13 +488,6 @@ class CacheGroup { resetCaching(): void; } -// @public (undocumented) -type CacheKey = [ -query: DocumentNode, -stringifiedVariables: string, -...queryKey: any[] -]; - // @public (undocumented) const enum CacheWriteBehavior { // (undocumented) @@ -549,6 +544,9 @@ export const concat: typeof ApolloLink.concat; // @public (undocumented) export const createHttpLink: (linkOptions?: HttpOptions) => ApolloLink; +// @alpha +export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; + // @public @deprecated (undocumented) export const createSignalIfSupported: () => { controller: boolean; @@ -582,12 +580,14 @@ export namespace DataProxy { } // (undocumented) export interface ReadFragmentOptions extends Fragment { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; } // (undocumented) export interface ReadQueryOptions extends Query { + // @deprecated canonizeResults?: boolean; optimistic?: boolean; returnPartialData?: boolean; @@ -708,14 +708,17 @@ export class DocumentTransform { // (undocumented) concat(otherTransform: DocumentTransform): DocumentTransform; // (undocumented) - getStableCacheEntry(document: DocumentNode): { - key: DocumentTransformCacheKey; - value?: DocumentNode | undefined; - } | undefined; - // (undocumented) static identity(): DocumentTransform; - // (undocumented) - static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform; + // @internal + readonly left?: DocumentTransform; + resetCache(): void; + // @internal + readonly right?: DocumentTransform; + // (undocumented) + static split(predicate: (document: DocumentNode) => boolean, left: DocumentTransform, right?: DocumentTransform): DocumentTransform & { + left: DocumentTransform; + right: DocumentTransform; + }; // (undocumented) transformDocument(document: DocumentNode): DocumentNode; } @@ -725,9 +728,7 @@ export type DocumentTransformCacheKey = ReadonlyArray; // @public (undocumented) interface DocumentTransformOptions { - // (undocumented) cache?: boolean; - // (undocumented) getCacheKey?: (document: DocumentNode) => DocumentTransformCacheKey | undefined; } @@ -792,7 +793,10 @@ abstract class EntityStore implements NormalizedCache { has(dataId: string): boolean; // (undocumented) protected lookup(dataId: string, dependOnExistence?: boolean): StoreObject | undefined; - // (undocumented) + makeCacheKey(document: DocumentNode, callback: Cache_2.WatchCallback, details: string): object; + makeCacheKey(selectionSet: SelectionSetNode, parent: string | StoreObject, varString: string | undefined, canonizeResults: boolean): object; + makeCacheKey(field: FieldNode, array: readonly any[], varString: string | undefined): object; + // @deprecated (undocumented) makeCacheKey(...args: any[]): object; // (undocumented) merge(older: string | StoreObject, newer: StoreObject | string): void; @@ -919,9 +923,7 @@ type FetchMoreOptions_2 = Parameters["fetchMore"]> export interface FetchMoreQueryOptions { // (undocumented) context?: DefaultContext; - // (undocumented) query?: DocumentNode | TypedDocumentNode; - // (undocumented) variables?: Partial; } @@ -1019,6 +1021,8 @@ interface FragmentRegistryAPI { // (undocumented) register(...fragments: DocumentNode[]): this; // (undocumented) + resetCaches(): void; + // (undocumented) transform(document: D): D; } @@ -1032,7 +1036,77 @@ export function fromError(errorValue: any): Observable; export function fromPromise(promise: Promise): Observable; // @public (undocumented) -export function getApolloContext(): React_2.Context; +interface FulfilledPromise extends Promise { + // (undocumented) + status: "fulfilled"; + // (undocumented) + value: TValue; +} + +// @internal +const getApolloCacheMemoryInternals: (() => { + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; + +// @internal +const getApolloClientMemoryInternals: (() => { + limits: { + [k: string]: number; + }; + sizes: { + cache?: { + fragmentQueryDocuments: number | undefined; + } | undefined; + addTypenameDocumentTransform?: { + cache: number; + }[] | undefined; + inMemoryCache?: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + } | undefined; + fragmentRegistry?: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + } | undefined; + print: number | undefined; + parser: number | undefined; + canonicalStringify: number | undefined; + links: unknown[]; + queryManager: { + getDocumentInfo: number; + documentTransforms: { + cache: number; + }[]; + }; + }; +}) | undefined; + +// @public (undocumented) +export function getApolloContext(): ReactTypes.Context; + +// @internal +const getInMemoryCacheMemoryInternals: (() => { + addTypenameDocumentTransform: { + cache: number; + }[]; + inMemoryCache: { + executeSelectionSet: number | undefined; + executeSubSelectedArray: number | undefined; + maybeBroadcastWatch: number | undefined; + }; + fragmentRegistry: { + findFragmentSpreads: number | undefined; + lookup: number | undefined; + transform: number | undefined; + }; + cache: { + fragmentQueryDocuments: number | undefined; + }; +}) | undefined; export { gql } @@ -1119,6 +1193,15 @@ export interface IDocumentDefinition { variables: ReadonlyArray; } +// @public (undocumented) +interface IgnoreModifier { + // (undocumented) + [_ignoreModifier]: true; +} + +// @public (undocumented) +const _ignoreModifier: unique symbol; + // @public (undocumented) export interface IncrementalPayload { // (undocumented) @@ -1157,6 +1240,10 @@ export class InMemoryCache extends ApolloCache { resetResultCache?: boolean; resetResultIdentities?: boolean; }): string[]; + // Warning: (ae-forgotten-export) The symbol "getInMemoryCacheMemoryInternals" needs to be exported by the entry point index.d.ts + // + // @internal + getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; // (undocumented) identify(object: StoreObject | Reference): string | undefined; // (undocumented) @@ -1189,7 +1276,7 @@ export class InMemoryCache extends ApolloCache { // @public (undocumented) export interface InMemoryCacheConfig extends ApolloReducerConfig { - // (undocumented) + // @deprecated (undocumented) canonizeResults?: boolean; // Warning: (ae-forgotten-export) The symbol "FragmentRegistryAPI" needs to be exported by the entry point index.d.ts // @@ -1197,7 +1284,7 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { fragments?: FragmentRegistryAPI; // (undocumented) possibleTypes?: PossibleTypesMap; - // (undocumented) + // @deprecated (undocumented) resultCacheMaxSize?: number; // (undocumented) resultCaching?: boolean; @@ -1208,34 +1295,38 @@ 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): Promise>; + 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) fetchMore(options: FetchMoreOptions_2): Promise>; - // Warning: (ae-forgotten-export) The symbol "CacheKey" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "QueryKey" needs to be exported by the entry point index.d.ts // // (undocumented) - readonly key: CacheKey; + 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-forgotten-export) The symbol "QueryRefPromise" needs to be exported by the entry point index.d.ts + // // (undocumented) - promise: Promise>; - // (undocumented) - promiseCache?: Map>>; + promise: QueryRefPromise; // (undocumented) refetch(variables: OperationVariables | undefined): Promise>; // (undocumented) + reinitialize(): void; + // (undocumented) result: ApolloQueryResult; // (undocumented) retain(): () => void; @@ -1248,8 +1339,6 @@ interface InternalQueryReferenceOptions { // (undocumented) autoDisposeTimeoutMs?: number; // (undocumented) - key: CacheKey; - // (undocumented) onDispose?: () => void; } @@ -1356,17 +1445,45 @@ export interface LazyQueryHookExecOptions extends Omit, "skip"> { +export interface LazyQueryHookOptions extends BaseQueryOptions { + // @internal (undocumented) + defaultOptions?: Partial>; + onCompleted?: (data: TData) => void; + onError?: (error: ApolloError) => void; } // @public @deprecated (undocumented) export type LazyQueryResult = QueryResult; // @public (undocumented) -export type LazyQueryResultTuple = [LazyQueryExecFunction, QueryResult]; +export type LazyQueryResultTuple = [ +execute: LazyQueryExecFunction, +result: QueryResult +]; + +// @public (undocumented) +type Listener = (promise: QueryRefPromise) => void; + +// @public (undocumented) +export type LoadableQueryHookFetchPolicy = Extract; + +// @public (undocumented) +export interface LoadableQueryHookOptions { + // @deprecated + canonizeResults?: boolean; + client?: ApolloClient; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: LoadableQueryHookFetchPolicy; + queryKey?: string | number | any[]; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; +} +// Warning: (ae-forgotten-export) The symbol "OnlyRequiredProperties" needs to be exported by the entry point index.d.ts +// // @public (undocumented) -type Listener = (promise: Promise>) => void; +export type LoadQueryFunction = (...args: [TVariables] extends [never] ? [] : {} extends OnlyRequiredProperties ? [variables?: TVariables] : [variables: TVariables]) => void; // @public (undocumented) class LocalState { @@ -1506,7 +1623,9 @@ interface MutationBaseOptions; - optimisticResponse?: TData | ((vars: TVariables) => TData); + optimisticResponse?: TData | ((vars: TVariables, { IGNORE }: { + IGNORE: IgnoreModifier; + }) => TData); refetchQueries?: ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; update?: MutationUpdaterFunction; updateQueries?: MutationQueryReducersMap; @@ -1527,7 +1646,6 @@ export type MutationFunction = ApolloCache> extends BaseMutationOptions { - // (undocumented) mutation?: DocumentNode | TypedDocumentNode; } @@ -1535,12 +1653,8 @@ export interface MutationFunctionOptions = ApolloCache> extends BaseMutationOptions { } -// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts -// // @public (undocumented) -export interface MutationOptions = ApolloCache> extends MutationBaseOptions { - fetchPolicy?: MutationFetchPolicy; - keepRootFields?: boolean; +export interface MutationOptions = ApolloCache> extends MutationSharedOptions { mutation: DocumentNode | TypedDocumentNode; } @@ -1560,20 +1674,22 @@ export type MutationQueryReducersMap { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data?: TData | null; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) reset(): void; } +// Warning: (ae-forgotten-export) The symbol "MutationBaseOptions" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +interface MutationSharedOptions = ApolloCache> extends MutationBaseOptions { + fetchPolicy?: MutationFetchPolicy; + keepRootFields?: boolean; +} + // @public (undocumented) interface MutationStoreValue { // (undocumented) @@ -1588,11 +1704,11 @@ interface MutationStoreValue { // @public (undocumented) export type MutationTuple = ApolloCache> = [ -(options?: MutationFunctionOptions) => Promise>, -MutationResult +mutate: (options?: MutationFunctionOptions) => Promise>, +result: MutationResult ]; -// @public (undocumented) +// @public @deprecated (undocumented) export type MutationUpdaterFn = (cache: ApolloCache, mutationResult: FetchResult) => void; @@ -1687,7 +1803,6 @@ export class ObservableQuery; }); - // (undocumented) fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { updateQuery?: (previousQueryResult: TData, options: { fetchMoreResult: TFetchData; @@ -1719,6 +1834,8 @@ export class ObservableQuery>, newNetworkStatus?: NetworkStatus): Concast>; + // @internal (undocumented) + resetDiff(): void; // (undocumented) resetLastResults(): void; // (undocumented) @@ -1734,20 +1851,30 @@ export class ObservableQuery | void>; // (undocumented) silentSetOptions(newOptions: Partial>): void; - // (undocumented) startPolling(pollInterval: number): void; - // (undocumented) stopPolling(): void; - // (undocumented) subscribeToMore(options: SubscribeToMoreOptions): () => void; - // (undocumented) updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; - // (undocumented) get variables(): TVariables | undefined; } // @public (undocumented) -export type ObservableQueryFields = Pick, "startPolling" | "stopPolling" | "subscribeToMore" | "updateQuery" | "refetch" | "reobserve" | "variables" | "fetchMore">; +export interface ObservableQueryFields { + fetchMore(fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: (previousQueryResult: TData, options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + }) => TData; + }): Promise>; + refetch(variables?: Partial): Promise>; + // @internal (undocumented) + reobserve(newOptions?: Partial>, newNetworkStatus?: NetworkStatus): Promise>; + startPolling(pollInterval: number): void; + stopPolling(): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: (previousQueryResult: TData, options: Pick, "variables">) => TData): void; + variables: TVariables | undefined; +} export { ObservableSubscription } @@ -1769,6 +1896,11 @@ export interface OnDataOptions { data: SubscriptionResult; } +// @public +type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; + // @public (undocumented) export type OnQueryUpdated = (observableQuery: ObservableQuery, diff: Cache_2.DiffResult, lastDiff: Cache_2.DiffResult | undefined) => boolean | TResult; @@ -1818,9 +1950,21 @@ export function parseAndCheckHttpResponse(operations: Operation | Operation[]): // @public (undocumented) export function parser(document: DocumentNode): IDocumentDefinition; +// @public (undocumented) +export namespace parser { + var // (undocumented) + resetCache: () => void; +} + // @public (undocumented) export type Path = ReadonlyArray; +// @public (undocumented) +interface PendingPromise extends Promise { + // (undocumented) + status: "pending"; +} + // @public (undocumented) class Policies { constructor(config: { @@ -1868,11 +2012,54 @@ export type PossibleTypesMap = { [supertype: string]: string[]; }; +// @public (undocumented) +export type PreloadQueryFetchPolicy = Extract; + +// @public +export interface PreloadQueryFunction { + // Warning: (ae-forgotten-export) The symbol "PreloadQueryOptionsArg" needs to be exported by the entry point index.d.ts + >(query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg, TOptions>): QueryReference | 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; +} + +// 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; + 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; // @public (undocumented) -const print_2: typeof print_3; +const print_2: ((ast: ASTNode) => string) & { + reset(): void; +}; // @public (undocumented) interface Printer { @@ -1882,6 +2069,16 @@ interface Printer { (node: ASTNode, originalPrint: typeof print_2): string; } +// @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; @@ -1896,20 +2093,16 @@ interface QueryData { // @public (undocumented) export interface QueryDataOptions extends QueryFunctionOptions { // (undocumented) - children?: (result: QueryResult) => ReactNode; - // (undocumented) + children?: (result: QueryResult) => ReactTypes.ReactNode; query: DocumentNode | TypedDocumentNode; } // @public (undocumented) -export interface QueryFunctionOptions extends BaseQueryOptions { - // (undocumented) +export interface QueryFunctionOptions extends BaseQueryOptions { + // @internal (undocumented) defaultOptions?: Partial>; - // (undocumented) onCompleted?: (data: TData) => void; - // (undocumented) onError?: (error: ApolloError) => void; - // (undocumented) skip?: boolean; } @@ -1945,7 +2138,7 @@ class QueryInfo { // 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): void; + markResult(result: FetchResult, document: DocumentNode, options: Pick, cacheWriteBehavior: CacheWriteBehavior): typeof result; // (undocumented) networkError?: Error | null; // (undocumented) @@ -1959,6 +2152,8 @@ class QueryInfo { // (undocumented) reset(): void; // (undocumented) + resetDiff(): void; + // (undocumented) resetLastWrite(): void; // (undocumented) setDiff(diff: Cache_2.DiffResult | null): void; @@ -1972,11 +2167,15 @@ class QueryInfo { variables?: Record; } +// @public (undocumented) +interface QueryKey { + // (undocumented) + __queryKey?: string; +} + // @public @deprecated (undocumented) export interface QueryLazyOptions { - // (undocumented) context?: DefaultContext; - // (undocumented) variables?: TVariables; } @@ -1985,7 +2184,7 @@ export type QueryListener = (queryInfo: QueryInfo) => void; // @public (undocumented) class QueryManager { - constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, }: { + constructor({ cache, link, defaultOptions, documentTransform, queryDeduplication, onBroadcast, ssrMode, clientAwareness, localState, assumeImmutableResults, defaultContext, }: { cache: ApolloCache; link: ApolloLink; defaultOptions?: DefaultOptions; @@ -1996,6 +2195,7 @@ class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }); // (undocumented) readonly assumeImmutableResults: boolean; @@ -2006,6 +2206,8 @@ class QueryManager { // (undocumented) clearStore(options?: Cache_2.ResetOptions): Promise; // (undocumented) + readonly defaultContext: Partial; + // (undocumented) defaultOptions: DefaultOptions; // (undocumented) readonly documentTransform: DocumentTransform; @@ -2032,7 +2234,9 @@ class QueryManager { // (undocumented) getQueryStore(): Record; // (undocumented) - protected inFlightLinkObservables: Map>>; + protected inFlightLinkObservables: Trie<{ + observable?: Observable> | undefined; + }>; // (undocumented) link: ApolloLink; // (undocumented) @@ -2046,7 +2250,7 @@ class QueryManager { updateQueries: UpdateQueries; update?: MutationUpdaterFunction; keepRootFields?: boolean; - }): void; + }): boolean; // (undocumented) markMutationResult>(mutation: { mutationId: string; @@ -2099,11 +2303,13 @@ class QueryManager { // @public interface QueryOptions { + // @deprecated canonizeResults?: boolean; context?: DefaultContext; errorPolicy?: ErrorPolicy; fetchPolicy?: FetchPolicy; notifyOnNetworkStatusChange?: boolean; + // @deprecated partialRefetch?: boolean; pollInterval?: number; query: DocumentNode | TypedDocumentNode; @@ -2116,30 +2322,31 @@ 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 { + // @internal (undocumented) + [PROMISE_SYMBOL]: QueryRefPromise; // Warning: (ae-forgotten-export) The symbol "InternalQueryReference" needs to be exported by the entry point index.d.ts // - // (undocumented) - [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // @internal (undocumented) + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + // @alpha + toPromise(): Promise>; } +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type QueryRefPromise = PromiseWithState>; + // @public (undocumented) export interface QueryResult extends ObservableQueryFields { - // (undocumented) called: boolean; - // (undocumented) client: ApolloClient; - // (undocumented) data: TData | undefined; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) networkStatus: NetworkStatus; - // (undocumented) observable: ObservableQuery; - // (undocumented) previousData?: TData; } @@ -2250,14 +2457,22 @@ export type RefetchQueryDescriptor = string | DocumentNode; // @public (undocumented) export type RefetchWritePolicy = "merge" | "overwrite"; +// @public (undocumented) +interface RejectedPromise extends Promise { + // (undocumented) + reason: unknown; + // (undocumented) + status: "rejected"; +} + // @public (undocumented) class RenderPromises { // (undocumented) - addObservableQueryPromise(obsQuery: ObservableQuery): ReactNode; + addObservableQueryPromise(obsQuery: ObservableQuery): ReactTypes.ReactNode; // Warning: (ae-forgotten-export) The symbol "QueryData" needs to be exported by the entry point index.d.ts // // (undocumented) - addQueryPromise(queryInstance: QueryData, finish?: () => React.ReactNode): React.ReactNode; + addQueryPromise(queryInstance: QueryData, finish?: () => ReactTypes.ReactNode): ReactTypes.ReactNode; // (undocumented) consumeAndAwaitPromises(): Promise; // (undocumented) @@ -2278,6 +2493,9 @@ export const resetApolloContext: typeof getApolloContext; export { resetCaches } +// @public (undocumented) +type ResetFunction = () => void; + // @public (undocumented) export type Resolver = (rootValue?: any, args?: any, context?: any, info?: { field: FieldNode; @@ -2342,6 +2560,26 @@ export type ServerParseError = Error & { export { setLogVerbosity } +// @public (undocumented) +interface SharedWatchQueryOptions { + // @deprecated + canonizeResults?: boolean; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; + fetchPolicy?: WatchQueryFetchPolicy; + initialFetchPolicy?: WatchQueryFetchPolicy; + // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts + nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); + notifyOnNetworkStatusChange?: boolean; + // @deprecated + partialRefetch?: boolean; + pollInterval?: number; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; + skipPollAttempt?: () => boolean; + variables?: TVariables; +} + // @public (undocumented) export interface SingleExecutionResult, TContext = DefaultContext, TExtensions = Record> extends ExecutionResult { // (undocumented) @@ -2433,13 +2671,10 @@ export interface SubscriptionOptions { - // (undocumented) data?: TData; - // (undocumented) error?: ApolloError; - // (undocumented) loading: boolean; - // (undocumented) + // @internal (undocumented) variables?: TVariables; } @@ -2447,13 +2682,19 @@ export interface SubscriptionResult { export type SuspenseQueryHookFetchPolicy = Extract; // @public (undocumented) -export interface SuspenseQueryHookOptions extends Pick, "client" | "variables" | "errorPolicy" | "context" | "canonizeResults" | "returnPartialData" | "refetchWritePolicy"> { - // (undocumented) +export interface SuspenseQueryHookOptions { + // @deprecated + canonizeResults?: boolean; + client?: ApolloClient; + context?: DefaultContext; + errorPolicy?: ErrorPolicy; fetchPolicy?: SuspenseQueryHookFetchPolicy; - // (undocumented) queryKey?: string | number | any[]; + refetchWritePolicy?: RefetchWritePolicy; + returnPartialData?: boolean; // @deprecated skip?: boolean; + variables?: TVariables; } // @public (undocumented) @@ -2544,7 +2785,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 ]; @@ -2553,7 +2794,7 @@ export function useBackgroundQuery | undefined>, +QueryReference | undefined, TVariables>, UseBackgroundQueryResult ]; @@ -2561,7 +2802,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { errorPolicy: "ignore" | "all"; }): [ -QueryReference, +QueryReference, UseBackgroundQueryResult ]; @@ -2570,7 +2811,7 @@ export function useBackgroundQuery> | undefined, +QueryReference, TVariables> | undefined, UseBackgroundQueryResult ]; @@ -2578,7 +2819,7 @@ UseBackgroundQueryResult export function useBackgroundQuery(query: DocumentNode | TypedDocumentNode, options: BackgroundQueryHookOptionsNoInfer & { returnPartialData: true; }): [ -QueryReference>, +QueryReference, TVariables>, UseBackgroundQueryResult ]; @@ -2586,12 +2827,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]; @@ -2600,13 +2844,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 ]; @@ -2638,16 +2882,58 @@ export type UseFragmentResult = { missing?: MissingTree; }; -// @public (undocumented) +// @public export function useLazyQuery(query: DocumentNode | TypedDocumentNode, options?: LazyQueryHookOptions, NoInfer>): LazyQueryResultTuple; // @public (undocumented) -export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions & TOptions): UseLoadableQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial : TData, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult | undefined, TVariables>; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; +}): UseLoadableQueryResult; + +// @public (undocumented) +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options: LoadableQueryHookOptions & { + returnPartialData: true; +}): UseLoadableQueryResult, TVariables>; + +// @public +export function useLoadableQuery(query: DocumentNode | TypedDocumentNode, options?: LoadableQueryHookOptions): UseLoadableQueryResult; // @public (undocumented) +export type UseLoadableQueryResult = [ +loadQuery: LoadQueryFunction, +queryRef: QueryReference | null, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; +} +]; + +// @public +export function useMutation = ApolloCache>(mutation: DocumentNode | TypedDocumentNode, options?: MutationHookOptions, NoInfer, TContext, TCache>): MutationTuple; + +// @public export function useQuery(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; +// @public +export function useQueryRefHandlers(queryRef: QueryReference): UseQueryRefHandlersResult; + // @public (undocumented) +export interface UseQueryRefHandlersResult { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; +} + +// @public export function useReactiveVar(rv: ReactiveVar): T; // @public (undocumented) @@ -2660,7 +2946,7 @@ export interface UseReadQueryResult { networkStatus: NetworkStatus; } -// @public (undocumented) +// @public export function useSubscription(subscription: DocumentNode | TypedDocumentNode, options?: SubscriptionHookOptions, NoInfer>): SubscriptionResult; // @public (undocumented) @@ -2724,25 +3010,23 @@ 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"; // @public -export interface WatchQueryOptions { - canonizeResults?: boolean; - context?: DefaultContext; - errorPolicy?: ErrorPolicy; - fetchPolicy?: WatchQueryFetchPolicy; - initialFetchPolicy?: WatchQueryFetchPolicy; - // Warning: (ae-forgotten-export) The symbol "NextFetchPolicyContext" needs to be exported by the entry point index.d.ts - nextFetchPolicy?: WatchQueryFetchPolicy | ((this: WatchQueryOptions, currentFetchPolicy: WatchQueryFetchPolicy, context: NextFetchPolicyContext) => WatchQueryFetchPolicy); - notifyOnNetworkStatusChange?: boolean; - partialRefetch?: boolean; - pollInterval?: number; +export interface WatchQueryOptions extends SharedWatchQueryOptions { query: DocumentNode | TypedDocumentNode; - refetchWritePolicy?: RefetchWritePolicy; - returnPartialData?: boolean; - variables?: TVariables; } // @public (undocumented) @@ -2779,19 +3063,21 @@ interface WriteContext extends ReadMergeModifyContext { // Warnings were encountered during analysis: // -// src/cache/inmemory/policies.ts:98:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/policies.ts:167:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts -// src/cache/inmemory/types.ts:132:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" 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:117:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:150:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts -// src/core/QueryManager.ts:380:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:253:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:92:3 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/policies.ts:161:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts +// src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:116:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:117:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:124:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:158:5 - (ae-forgotten-export) The symbol "LocalState" needs to be exported by the entry point index.d.ts +// src/core/QueryManager.ts:399:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "IgnoreModifier" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:269:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/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:26:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:27:3 - (ae-forgotten-export) The symbol "RefetchFunction" 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:106:1 - (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/beige-geese-wink.md b/.changeset/beige-geese-wink.md new file mode 100644 index 00000000000..d92e77ccb9d --- /dev/null +++ b/.changeset/beige-geese-wink.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Decouple `canonicalStringify` from `ObjectCanon` for better time and memory performance. diff --git a/.changeset/breezy-spiders-tap.md b/.changeset/breezy-spiders-tap.md new file mode 100644 index 00000000000..a8af04cea0d --- /dev/null +++ b/.changeset/breezy-spiders-tap.md @@ -0,0 +1,38 @@ +--- +"@apollo/client": patch +--- + +Add a `defaultContext` option and property on `ApolloClient`, e.g. for keeping track of changing auth tokens or dependency injection. + +This can be used e.g. in authentication scenarios, where a new token might be +generated outside of the link chain and should passed into the link chain. + +```js +import { ApolloClient, createHttpLink, InMemoryCache } from '@apollo/client'; +import { setContext } from '@apollo/client/link/context'; + +const httpLink = createHttpLink({ + uri: '/graphql', +}); + +const authLink = setContext((_, { headers, token }) => { + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : "", + } + } +}); + +const client = new ApolloClient({ + link: authLink.concat(httpLink), + cache: new InMemoryCache() +}); + +// somewhere else in your application +function onNewToken(newToken) { + // token can now be changed for future requests without need for a global + // variable, scoped ref or recreating the client + client.defaultContext.token = newToken +} +``` diff --git a/.changeset/chatty-comics-yawn.md b/.changeset/chatty-comics-yawn.md new file mode 100644 index 00000000000..b50a939eda2 --- /dev/null +++ b/.changeset/chatty-comics-yawn.md @@ -0,0 +1,8 @@ +--- +"@apollo/client": patch +--- + +Adds a deprecation warning to the HOC and render prop APIs. + +The HOC and render prop APIs have already been deprecated since 2020, +but we previously didn't have a @deprecated tag in the DocBlocks. diff --git a/.changeset/clean-items-smash.md b/.changeset/clean-items-smash.md new file mode 100644 index 00000000000..c0111542c78 --- /dev/null +++ b/.changeset/clean-items-smash.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fix a potential memory leak in `FragmentRegistry.transform` and `FragmentRegistry.findFragmentSpreads` that would hold on to passed-in `DocumentNodes` for too long. diff --git a/.changeset/cold-llamas-turn.md b/.changeset/cold-llamas-turn.md new file mode 100644 index 00000000000..a3f1e0099df --- /dev/null +++ b/.changeset/cold-llamas-turn.md @@ -0,0 +1,8 @@ +--- +"@apollo/client": patch +--- + +`parse` function: improve memory management +* use LRU `WeakCache` instead of `Map` to keep a limited number of parsed results +* cache is initiated lazily, only when needed +* expose `parse.resetCache()` method diff --git a/.changeset/curvy-seas-hope.md b/.changeset/curvy-seas-hope.md new file mode 100644 index 00000000000..65491ac6318 --- /dev/null +++ b/.changeset/curvy-seas-hope.md @@ -0,0 +1,13 @@ +--- +"@apollo/client": minor +--- + +Simplify RetryLink, fix potential memory leak + +Historically, `RetryLink` would keep a `values` array of all previous values, +in case the operation would get an additional subscriber at a later point in time. +In practice, this could lead to a memory leak (#11393) and did not serve any +further purpose, as the resulting observable would only be subscribed to by +Apollo Client itself, and only once - it would be wrapped in a `Concast` before +being exposed to the user, and that `Concast` would handle subscribers on its +own. diff --git a/.changeset/dirty-kids-crash.md b/.changeset/dirty-kids-crash.md new file mode 100644 index 00000000000..504c049268d --- /dev/null +++ b/.changeset/dirty-kids-crash.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`documentTransform`: use `optimism` and `WeakCache` instead of directly storing data on the `Trie` diff --git a/.changeset/dirty-tigers-matter.md b/.changeset/dirty-tigers-matter.md new file mode 100644 index 00000000000..1a5d4a9e195 --- /dev/null +++ b/.changeset/dirty-tigers-matter.md @@ -0,0 +1,13 @@ +--- +"@apollo/client": minor +--- + +Create a new `useQueryRefHandlers` hook that returns `refetch` and `fetchMore` functions for a given `queryRef`. This is useful to get access to handlers for a `queryRef` that was created by `createQueryPreloader` or when the handlers for a `queryRef` produced by a different component are inaccessible. + +```jsx +const MyComponent({ queryRef }) { + const { refetch, fetchMore } = useQueryRefHandlers(queryRef); + + // ... +} +``` diff --git a/.changeset/forty-cups-shop.md b/.changeset/forty-cups-shop.md new file mode 100644 index 00000000000..2c576843fdd --- /dev/null +++ b/.changeset/forty-cups-shop.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Fixes a potential memory leak in `Concast` that might have been triggered when `Concast` was used outside of Apollo Client. diff --git a/.changeset/friendly-clouds-laugh.md b/.changeset/friendly-clouds-laugh.md new file mode 100644 index 00000000000..3821053fa83 --- /dev/null +++ b/.changeset/friendly-clouds-laugh.md @@ -0,0 +1,7 @@ +--- +"@apollo/client": minor +--- + +To work around issues in React Server Components, especially with bundling for +the Next.js "edge" runtime we now use an external package to wrap `react` imports +instead of importing React directly. diff --git a/.changeset/hot-ducks-burn.md b/.changeset/hot-ducks-burn.md new file mode 100644 index 00000000000..c0f8ac1836c --- /dev/null +++ b/.changeset/hot-ducks-burn.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Add a `resetCache` method to `DocumentTransform` and hook `InMemoryCache.addTypenameTransform` up to `InMemoryCache.gc` diff --git a/.changeset/late-rabbits-protect.md b/.changeset/late-rabbits-protect.md new file mode 100644 index 00000000000..1494b569018 --- /dev/null +++ b/.changeset/late-rabbits-protect.md @@ -0,0 +1,7 @@ +--- +'@apollo/client': minor +--- + +Remove the need to call `retain` from `useLoadableQuery` since `useReadQuery` will now retain the query. This means that a `queryRef` that is not consumed by `useReadQuery` within the given `autoDisposeTimeoutMs` will now be auto diposed for you. + +Thanks to [#11412](https://github.com/apollographql/apollo-client/pull/11412), disposed query refs will be automatically resubscribed to the query when consumed by `useReadQuery` after it has been disposed. diff --git a/.changeset/mighty-coats-check.md b/.changeset/mighty-coats-check.md new file mode 100644 index 00000000000..0d80272f8a5 --- /dev/null +++ b/.changeset/mighty-coats-check.md @@ -0,0 +1,47 @@ +--- +"@apollo/client": minor +--- + +Allow returning `IGNORE` sentinel object from `optimisticResponse` functions to bail-out from the optimistic update. + +Consider this example: + +```jsx +const UPDATE_COMMENT = gql` + mutation UpdateComment($commentId: ID!, $commentContent: String!) { + updateComment(commentId: $commentId, content: $commentContent) { + id + __typename + content + } + } +`; + +function CommentPageWithData() { + const [mutate] = useMutation(UPDATE_COMMENT); + return ( + + mutate({ + variables: { commentId, commentContent }, + optimisticResponse: (vars, { IGNORE }) => { + if (commentContent === "foo") { + // conditionally bail out of optimistic updates + return IGNORE; + } + return { + updateComment: { + id: commentId, + __typename: "Comment", + content: commentContent + } + } + }, + }) + } + /> + ); +} +``` + +The `IGNORE` sentinel can be destructured from the second parameter in the callback function signature passed to `optimisticResponse`. diff --git a/.changeset/pink-apricots-yawn.md b/.changeset/pink-apricots-yawn.md new file mode 100644 index 00000000000..6eec10853be --- /dev/null +++ b/.changeset/pink-apricots-yawn.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Increase the default memory limits for `executeSelectionSet` and `executeSelectionSetArray`. diff --git a/.changeset/polite-avocados-warn.md b/.changeset/polite-avocados-warn.md new file mode 100644 index 00000000000..dd04015cf3d --- /dev/null +++ b/.changeset/polite-avocados-warn.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`print`: use `WeakCache` instead of `WeakMap` diff --git a/.changeset/quick-hats-marry.md b/.changeset/quick-hats-marry.md new file mode 100644 index 00000000000..2667f0a9750 --- /dev/null +++ b/.changeset/quick-hats-marry.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +ensure `defaultContext` is also used for mutations and subscriptions 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/.changeset/shaggy-ears-scream.md b/.changeset/shaggy-ears-scream.md new file mode 100644 index 00000000000..3ec33bfab58 --- /dev/null +++ b/.changeset/shaggy-ears-scream.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Prevent `QueryInfo#markResult` mutation of `result.data` and return cache data consistently whether complete or incomplete. diff --git a/.changeset/shaggy-sheep-pull.md b/.changeset/shaggy-sheep-pull.md new file mode 100644 index 00000000000..9c4ac23123b --- /dev/null +++ b/.changeset/shaggy-sheep-pull.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +`QueryManager.transformCache`: use `WeakCache` instead of `WeakMap` diff --git a/.changeset/six-rocks-arrive.md b/.changeset/six-rocks-arrive.md new file mode 100644 index 00000000000..19b433d8439 --- /dev/null +++ b/.changeset/six-rocks-arrive.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Release changes from [`v3.8.10`](https://github.com/apollographql/apollo-client/releases/tag/v3.8.10) diff --git a/.changeset/sixty-boxes-rest.md b/.changeset/sixty-boxes-rest.md new file mode 100644 index 00000000000..cce6eb7a98a --- /dev/null +++ b/.changeset/sixty-boxes-rest.md @@ -0,0 +1,8 @@ +--- +"@apollo/client": minor +--- + +`QueryManager.inFlightLinkObservables` now uses a strong `Trie` as an internal data structure. + +#### Warning: requires `@apollo/experimental-nextjs-app-support` update +If you are using `@apollo/experimental-nextjs-app-support`, you will need to update that to at least 0.5.2, as it accesses this internal data structure. diff --git a/.changeset/smooth-plums-shout.md b/.changeset/smooth-plums-shout.md new file mode 100644 index 00000000000..909e07ede8f --- /dev/null +++ b/.changeset/smooth-plums-shout.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +ObservableQuery: prevent reporting results of previous queries if the variables changed since diff --git a/.changeset/sour-sheep-walk.md b/.changeset/sour-sheep-walk.md new file mode 100644 index 00000000000..b0270d5ee68 --- /dev/null +++ b/.changeset/sour-sheep-walk.md @@ -0,0 +1,7 @@ +--- +"@apollo/client": minor +--- + +Ability to dynamically match mocks + +Adds support for a new property `MockedResponse.variableMatcher`: a predicate function that accepts a `variables` param. If `true`, the `variables` will be passed into the `ResultFunction` to help dynamically build a response. diff --git a/.changeset/spicy-drinks-camp.md b/.changeset/spicy-drinks-camp.md new file mode 100644 index 00000000000..24c9d189945 --- /dev/null +++ b/.changeset/spicy-drinks-camp.md @@ -0,0 +1,5 @@ +--- +'@apollo/client': patch +--- + +Address bundling issue introduced in [#11412](https://github.com/apollographql/apollo-client/pull/11412) where the `react/cache` internals ended up duplicated in the bundle. This was due to the fact that we had a `react/hooks` entrypoint that imported these files along with the newly introduced `createQueryPreloader` function, which lived outside of the `react/hooks` folder. diff --git a/.changeset/strong-terms-perform.md b/.changeset/strong-terms-perform.md new file mode 100644 index 00000000000..6974100076e --- /dev/null +++ b/.changeset/strong-terms-perform.md @@ -0,0 +1,46 @@ +--- +"@apollo/client": minor +--- + +Add multipart subscription network adapters for Relay and urql + +### Relay + +```tsx +import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/relay"; +import { Environment, Network, RecordSource, Store } from "relay-runtime"; + +const fetchMultipartSubs = createFetchMultipartSubscription( + "http://localhost:4000" +); + +const network = Network.create(fetchQuery, fetchMultipartSubs); + +export const RelayEnvironment = new Environment({ + network, + store: new Store(new RecordSource()), +}); +``` + +### Urql + +```tsx +import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/urql"; +import { Client, fetchExchange, subscriptionExchange } from "@urql/core"; + +const url = "http://localhost:4000"; + +const multipartSubscriptionForwarder = createFetchMultipartSubscription( + url +); + +const client = new Client({ + url, + exchanges: [ + fetchExchange, + subscriptionExchange({ + forwardSubscription: multipartSubscriptionForwarder, + }), + ], +}); +``` diff --git a/.changeset/swift-zoos-collect.md b/.changeset/swift-zoos-collect.md new file mode 100644 index 00000000000..b3e988b8f0a --- /dev/null +++ b/.changeset/swift-zoos-collect.md @@ -0,0 +1,19 @@ +--- +"@apollo/client": minor +--- + +Adds a new `skipPollAttempt` callback function that's called whenever a refetch attempt occurs while polling. If the function returns `true`, the refetch is skipped and not reattempted until the next poll interval. This will solve the frequent use-case of disabling polling when the window is inactive. + +```ts +useQuery(QUERY, { + pollInterval: 1000, + skipPollAttempt: () => document.hidden // or !document.hasFocus() +}); +// or define it globally +new ApolloClient({ + defaultOptions: { + watchQuery: { + skipPollAttempt: () => document.hidden // or !document.hasFocus() + } + } +}) diff --git a/.changeset/thick-mice-collect.md b/.changeset/thick-mice-collect.md new file mode 100644 index 00000000000..47ed2e58cfd --- /dev/null +++ b/.changeset/thick-mice-collect.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Clarify types of `EntityStore.makeCacheKey`. diff --git a/.changeset/thick-tips-cry.md b/.changeset/thick-tips-cry.md new file mode 100644 index 00000000000..407513ec1c7 --- /dev/null +++ b/.changeset/thick-tips-cry.md @@ -0,0 +1,9 @@ +--- +"@apollo/client": patch +--- + +Persisted Query Link: improve memory management +* use LRU `WeakCache` instead of `WeakMap` to keep a limited number of hash results +* hash cache is initiated lazily, only when needed +* expose `persistedLink.resetHashCache()` method +* reset hash cache if the upstream server reports it doesn't accept persisted queries diff --git a/.changeset/thirty-ties-arrive.md b/.changeset/thirty-ties-arrive.md new file mode 100644 index 00000000000..c8a6fc22c86 --- /dev/null +++ b/.changeset/thirty-ties-arrive.md @@ -0,0 +1,26 @@ +--- +"@apollo/client": minor +--- + +Introduces a new `useLoadableQuery` hook. This hook works similarly to `useBackgroundQuery` in that it returns a `queryRef` that can be used to suspend a component via the `useReadQuery` hook. It provides a more ergonomic way to load the query during a user interaction (for example when wanting to preload some data) that would otherwise be clunky with `useBackgroundQuery`. + +```tsx +function App() { + const [loadQuery, queryRef, { refetch, fetchMore, reset }] = useLoadableQuery(query, options) + + return ( + <> + + }> + {queryRef && } + + + ); +} + +function Child({ queryRef }) { + const { data } = useReadQuery(queryRef) + + // ... +} +``` diff --git a/.changeset/tough-timers-begin.md b/.changeset/tough-timers-begin.md new file mode 100644 index 00000000000..53fac70e002 --- /dev/null +++ b/.changeset/tough-timers-begin.md @@ -0,0 +1,8 @@ +--- +"@apollo/client": minor +--- + +Deprecates `canonizeResults`. + +Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +A future version of Apollo Client will contain a similar feature without the risk of memory leaks. diff --git a/.changeset/unlucky-rats-decide.md b/.changeset/unlucky-rats-decide.md new file mode 100644 index 00000000000..9be1d2d3961 --- /dev/null +++ b/.changeset/unlucky-rats-decide.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +use WeakMap in React Native with Hermes diff --git a/.changeset/violet-lions-draw.md b/.changeset/violet-lions-draw.md new file mode 100644 index 00000000000..6e5d046a6c9 --- /dev/null +++ b/.changeset/violet-lions-draw.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +InMemoryCache.gc now also triggers FragmentRegistry.resetCaches (if there is a FragmentRegistry) diff --git a/.changeset/wet-forks-rhyme.md b/.changeset/wet-forks-rhyme.md new file mode 100644 index 00000000000..2fc57066943 --- /dev/null +++ b/.changeset/wet-forks-rhyme.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Adds an experimental `ApolloClient.getMemoryInternals` helper diff --git a/.changeset/wild-dolphins-jog.md b/.changeset/wild-dolphins-jog.md new file mode 100644 index 00000000000..8030414fffe --- /dev/null +++ b/.changeset/wild-dolphins-jog.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Add `reset` method to `print`, hook up to `InMemoryCache.gc` diff --git a/.changeset/wise-news-grab.md b/.changeset/wise-news-grab.md new file mode 100644 index 00000000000..83eafb1375f --- /dev/null +++ b/.changeset/wise-news-grab.md @@ -0,0 +1,7 @@ +--- +'@apollo/client': minor +--- + +Remove the need to call `retain` from `useBackgroundQuery` since `useReadQuery` will now retain the query. This means that a `queryRef` that is not consumed by `useReadQuery` within the given `autoDisposeTimeoutMs` will now be auto diposed for you. + +Thanks to [#11412](https://github.com/apollographql/apollo-client/pull/11412), disposed query refs will be automatically resubscribed to the query when consumed by `useReadQuery` after it has been disposed. diff --git a/.changeset/yellow-flies-repeat.md b/.changeset/yellow-flies-repeat.md new file mode 100644 index 00000000000..b6fcff7db25 --- /dev/null +++ b/.changeset/yellow-flies-repeat.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": minor +--- + +Support re-using of mocks in the MockedProvider diff --git a/.circleci/config.yml b/.circleci/config.yml index 3d722d65051..5ca7fe4ee14 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -89,6 +89,24 @@ jobs: command: npm run test --workspace=<< parameters.framework >> working_directory: integration-tests + TestPeerDepTypes: + parameters: + externalPackage: + type: string + docker: + - image: cimg/node:20.6.1 + steps: + - checkout + - attach_workspace: + at: /tmp/workspace + - run: + working_directory: integration-tests/peerdeps-tsc + command: | + npm install + npm install @apollo/client@/tmp/workspace/apollo-client.tgz + npm install << parameters.externalPackage >> + npm test + workflows: Build and Test: jobs: @@ -112,6 +130,20 @@ workflows: - vite - vite-swc # -browser-esm would need a package publish to npm/CDNs + - TestPeerDepTypes: + name: Test external types for << matrix.externalPackage >> + requires: + - BuildTarball + matrix: + parameters: + externalPackage: + - "graphql@15" + - "graphql@16" + - "graphql@^17.0.0-alpha" + - "@types/react@16.8 @types/react-dom@16.8" + - "@types/react@17 @types/react-dom@17" + - "@types/react@18 @types/react-dom@18" + - "typescript@next" security-scans: jobs: - secops/gitleaks: diff --git a/.github/workflows/api-extractor.yml b/.github/workflows/api-extractor.yml index d9d3cb5bc45..b54d5e078aa 100644 --- a/.github/workflows/api-extractor.yml +++ b/.github/workflows/api-extractor.yml @@ -19,8 +19,6 @@ jobs: - name: Install dependencies (with cache) uses: bahmutov/npm-install@v1 - - name: Run build - run: npm run build - + # Builds the library and runs the api extractor - name: Run Api-Extractor run: npm run extract-api diff --git a/.prettierignore b/.prettierignore index 6e87bed035a..fe391b018fc 100644 --- a/.prettierignore +++ b/.prettierignore @@ -29,7 +29,10 @@ /docs/shared/** !/docs/shared/ApiDoc !/docs/shared/ApiDoc/** +!/docs/shared/Overrides +!/docs/shared/Overrides/** node_modules/ .yalc/ .next/ +.changeset/ diff --git a/.size-limit.cjs b/.size-limit.cjs index 5d8c63f865d..a91cfa60a04 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -20,6 +20,7 @@ const checks = [ "useSubscription", "useSuspenseQuery", "useBackgroundQuery", + "useLoadableQuery", "useReadQuery", "useFragment", ].map((name) => ({ path: "dist/react/index.js", import: `{ ${name} }` })), @@ -39,6 +40,7 @@ const checks = [ "react", "react-dom", "@graphql-typed-document-node/core", + "@wry/caches", "@wry/context", "@wry/equality", "@wry/trie", diff --git a/.size-limits.json b/.size-limits.json index 96d031aaf5d..6940b76cd93 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 37930, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 31974 + "dist/apollo-client.min.cjs": 39154, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32652 } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6718001cd0f..341779e9754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # @apollo/client +## 3.9.0-rc.1 + +### Patch Changes + +- [#11503](https://github.com/apollographql/apollo-client/pull/11503) [`67f62e3`](https://github.com/apollographql/apollo-client/commit/67f62e359bc471787d066319326e5582b4a635c8) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Release changes from [`v3.8.10`](https://github.com/apollographql/apollo-client/releases/tag/v3.8.10) + ## 3.8.10 ### Patch Changes @@ -8,6 +14,12 @@ - [#11483](https://github.com/apollographql/apollo-client/pull/11483) [`6394dda`](https://github.com/apollographql/apollo-client/commit/6394dda47fa83d9ddd922e0d05e62bd872e4ea8e) Thanks [@pipopotamasu](https://github.com/pipopotamasu)! - Fix cache override warning output +## 3.9.0-rc.0 + +### Minor Changes + +- [#11495](https://github.com/apollographql/apollo-client/pull/11495) [`1190aa5`](https://github.com/apollographql/apollo-client/commit/1190aa59a106217f7192c1f81099adfa5e4365c1) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Increase the default memory limits for `executeSelectionSet` and `executeSelectionSetArray`. + ## 3.8.9 ### Patch Changes @@ -20,6 +32,213 @@ - [#11470](https://github.com/apollographql/apollo-client/pull/11470) [`e293bc9`](https://github.com/apollographql/apollo-client/commit/e293bc90d6f7937a6fc7c169f7b16eeb39d5fd49) Thanks [@phryneas](https://github.com/phryneas)! - Remove an unnecessary check from parseAndCheckHttpResponse. +## 3.9.0-beta.1 + +### Minor Changes + +- [#11424](https://github.com/apollographql/apollo-client/pull/11424) [`62f3b6d`](https://github.com/apollographql/apollo-client/commit/62f3b6d0e89611e27d9f29812ee60e5db5963fd6) Thanks [@phryneas](https://github.com/phryneas)! - Simplify RetryLink, fix potential memory leak + + Historically, `RetryLink` would keep a `values` array of all previous values, + in case the operation would get an additional subscriber at a later point in time. + In practice, this could lead to a memory leak (#11393) and did not serve any + further purpose, as the resulting observable would only be subscribed to by + Apollo Client itself, and only once - it would be wrapped in a `Concast` before + being exposed to the user, and that `Concast` would handle subscribers on its + own. + +- [#11442](https://github.com/apollographql/apollo-client/pull/11442) [`4b6f2bc`](https://github.com/apollographql/apollo-client/commit/4b6f2bccf3ba94643b38689b32edd2839e47aec1) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Remove the need to call `retain` from `useLoadableQuery` since `useReadQuery` will now retain the query. This means that a `queryRef` that is not consumed by `useReadQuery` within the given `autoDisposeTimeoutMs` will now be auto diposed for you. + + Thanks to [#11412](https://github.com/apollographql/apollo-client/pull/11412), disposed query refs will be automatically resubscribed to the query when consumed by `useReadQuery` after it has been disposed. + +- [#11438](https://github.com/apollographql/apollo-client/pull/11438) [`6d46ab9`](https://github.com/apollographql/apollo-client/commit/6d46ab930a5e9bd5cae153d3b75b8966784fcd4e) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Remove the need to call `retain` from `useBackgroundQuery` since `useReadQuery` will now retain the query. This means that a `queryRef` that is not consumed by `useReadQuery` within the given `autoDisposeTimeoutMs` will now be auto diposed for you. + + Thanks to [#11412](https://github.com/apollographql/apollo-client/pull/11412), disposed query refs will be automatically resubscribed to the query when consumed by `useReadQuery` after it has been disposed. + +### Patch Changes + +- [#11443](https://github.com/apollographql/apollo-client/pull/11443) [`ff5a332`](https://github.com/apollographql/apollo-client/commit/ff5a332ff8b190c418df25371e36719d70061ebe) Thanks [@phryneas](https://github.com/phryneas)! - Adds a deprecation warning to the HOC and render prop APIs. + + The HOC and render prop APIs have already been deprecated since 2020, + but we previously didn't have a @deprecated tag in the DocBlocks. + +- [#11078](https://github.com/apollographql/apollo-client/pull/11078) [`14edebe`](https://github.com/apollographql/apollo-client/commit/14edebebefb7634c32b921d02c1c85c6c8737989) Thanks [@phryneas](https://github.com/phryneas)! - ObservableQuery: prevent reporting results of previous queries if the variables changed since + +- [#11439](https://github.com/apollographql/apollo-client/pull/11439) [`33454f0`](https://github.com/apollographql/apollo-client/commit/33454f0a40a05ea2b00633bda20a84d0ec3a4f4d) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Address bundling issue introduced in [#11412](https://github.com/apollographql/apollo-client/pull/11412) where the `react/cache` internals ended up duplicated in the bundle. This was due to the fact that we had a `react/hooks` entrypoint that imported these files along with the newly introduced `createQueryPreloader` function, which lived outside of the `react/hooks` folder. + +## 3.9.0-beta.0 + +### Minor Changes + +- [#11412](https://github.com/apollographql/apollo-client/pull/11412) [`58db5c3`](https://github.com/apollographql/apollo-client/commit/58db5c3295b88162f91019f0898f6baa4b9cced6) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Create a new `useQueryRefHandlers` hook that returns `refetch` and `fetchMore` functions for a given `queryRef`. This is useful to get access to handlers for a `queryRef` that was created by `createQueryPreloader` or when the handlers for a `queryRef` produced by a different component are inaccessible. + + ```jsx + const MyComponent({ queryRef }) { + const { refetch, fetchMore } = useQueryRefHandlers(queryRef); + + // ... + } + ``` + +- [#11410](https://github.com/apollographql/apollo-client/pull/11410) [`07fcf6a`](https://github.com/apollographql/apollo-client/commit/07fcf6a3bf5bc78ffe6f3e598897246b4da02cbb) Thanks [@sf-twingate](https://github.com/sf-twingate)! - Allow returning `IGNORE` sentinel object from `optimisticResponse` functions to bail-out from the optimistic update. + + Consider this example: + + ```jsx + const UPDATE_COMMENT = gql` + mutation UpdateComment($commentId: ID!, $commentContent: String!) { + updateComment(commentId: $commentId, content: $commentContent) { + id + __typename + content + } + } + `; + + function CommentPageWithData() { + const [mutate] = useMutation(UPDATE_COMMENT); + return ( + + mutate({ + variables: { commentId, commentContent }, + optimisticResponse: (vars, { IGNORE }) => { + if (commentContent === "foo") { + // conditionally bail out of optimistic updates + return IGNORE; + } + return { + updateComment: { + id: commentId, + __typename: "Comment", + content: commentContent, + }, + }; + }, + }) + } + /> + ); + } + ``` + + The `IGNORE` sentinel can be destructured from the second parameter in the callback function signature passed to `optimisticResponse`. + +- [#11412](https://github.com/apollographql/apollo-client/pull/11412) [`58db5c3`](https://github.com/apollographql/apollo-client/commit/58db5c3295b88162f91019f0898f6baa4b9cced6) Thanks [@jerelmiller](https://github.com/jerelmiller)! - 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 + } + ``` + +- [#11397](https://github.com/apollographql/apollo-client/pull/11397) [`3f7eecb`](https://github.com/apollographql/apollo-client/commit/3f7eecbfbd4f4444cffcaac7dd9fd225c8c2a401) Thanks [@aditya-kumawat](https://github.com/aditya-kumawat)! - Adds a new `skipPollAttempt` callback function that's called whenever a refetch attempt occurs while polling. If the function returns `true`, the refetch is skipped and not reattempted until the next poll interval. This will solve the frequent use-case of disabling polling when the window is inactive. + + ```ts + useQuery(QUERY, { + pollInterval: 1000, + skipPollAttempt: () => document.hidden, // or !document.hasFocus() + }); + // or define it globally + new ApolloClient({ + defaultOptions: { + watchQuery: { + skipPollAttempt: () => document.hidden, // or !document.hasFocus() + }, + }, + }); + ``` + +- [#11435](https://github.com/apollographql/apollo-client/pull/11435) [`5cce53e`](https://github.com/apollographql/apollo-client/commit/5cce53e83b976f85d2d2b06e28cc38f01324fea1) Thanks [@phryneas](https://github.com/phryneas)! - Deprecates `canonizeResults`. + + Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. + A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + +### Patch Changes + +- [#11369](https://github.com/apollographql/apollo-client/pull/11369) [`2a47164`](https://github.com/apollographql/apollo-client/commit/2a471646616e3af1b5c039e961f8d5717fad8f32) Thanks [@phryneas](https://github.com/phryneas)! - Persisted Query Link: improve memory management + + - use LRU `WeakCache` instead of `WeakMap` to keep a limited number of hash results + - hash cache is initiated lazily, only when needed + - expose `persistedLink.resetHashCache()` method + - reset hash cache if the upstream server reports it doesn't accept persisted queries + +- [#10804](https://github.com/apollographql/apollo-client/pull/10804) [`221dd99`](https://github.com/apollographql/apollo-client/commit/221dd99ffd1990f8bd0392543af35e9b08d0fed8) Thanks [@phryneas](https://github.com/phryneas)! - use WeakMap in React Native with Hermes + +- [#11409](https://github.com/apollographql/apollo-client/pull/11409) [`2e7203b`](https://github.com/apollographql/apollo-client/commit/2e7203b3a9618952ddb522627ded7cceabd7f250) Thanks [@phryneas](https://github.com/phryneas)! - Adds an experimental `ApolloClient.getMemoryInternals` helper + +## 3.9.0-alpha.5 + +### Minor Changes + +- [#11345](https://github.com/apollographql/apollo-client/pull/11345) [`1759066a8`](https://github.com/apollographql/apollo-client/commit/1759066a8f9a204e49228568aef9446a64890ff3) Thanks [@phryneas](https://github.com/phryneas)! - `QueryManager.inFlightLinkObservables` now uses a strong `Trie` as an internal data structure. + + #### Warning: requires `@apollo/experimental-nextjs-app-support` update + + If you are using `@apollo/experimental-nextjs-app-support`, you will need to update that to at least 0.5.2, as it accesses this internal data structure. + +- [#11300](https://github.com/apollographql/apollo-client/pull/11300) [`a8158733c`](https://github.com/apollographql/apollo-client/commit/a8158733cfa3e65180ec23518d657ea41894bb2b) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Introduces a new `useLoadableQuery` hook. This hook works similarly to `useBackgroundQuery` in that it returns a `queryRef` that can be used to suspend a component via the `useReadQuery` hook. It provides a more ergonomic way to load the query during a user interaction (for example when wanting to preload some data) that would otherwise be clunky with `useBackgroundQuery`. + + ```tsx + function App() { + const [loadQuery, queryRef, { refetch, fetchMore, reset }] = + useLoadableQuery(query, options); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Child({ queryRef }) { + const { data } = useReadQuery(queryRef); + + // ... + } + ``` + +### Patch Changes + +- [#11356](https://github.com/apollographql/apollo-client/pull/11356) [`cc4ac7e19`](https://github.com/apollographql/apollo-client/commit/cc4ac7e1917f046bcd177882727864eed40b910e) Thanks [@phryneas](https://github.com/phryneas)! - Fix a potential memory leak in `FragmentRegistry.transform` and `FragmentRegistry.findFragmentSpreads` that would hold on to passed-in `DocumentNodes` for too long. + +- [#11370](https://github.com/apollographql/apollo-client/pull/11370) [`25e2cb431`](https://github.com/apollographql/apollo-client/commit/25e2cb431c76ec5aa88202eaacbd98fad42edc7f) Thanks [@phryneas](https://github.com/phryneas)! - `parse` function: improve memory management + + - use LRU `WeakCache` instead of `Map` to keep a limited number of parsed results + - cache is initiated lazily, only when needed + - expose `parse.resetCache()` method + +- [#11389](https://github.com/apollographql/apollo-client/pull/11389) [`139acd115`](https://github.com/apollographql/apollo-client/commit/139acd1153afa1445b69dcb4e139668ab8c5889a) Thanks [@phryneas](https://github.com/phryneas)! - `documentTransform`: use `optimism` and `WeakCache` instead of directly storing data on the `Trie` + +- [#11358](https://github.com/apollographql/apollo-client/pull/11358) [`7d939f80f`](https://github.com/apollographql/apollo-client/commit/7d939f80fbc2c419c58a6c55b6a35ee7474d0379) Thanks [@phryneas](https://github.com/phryneas)! - Fixes a potential memory leak in `Concast` that might have been triggered when `Concast` was used outside of Apollo Client. + +- [#11344](https://github.com/apollographql/apollo-client/pull/11344) [`bd2667619`](https://github.com/apollographql/apollo-client/commit/bd2667619700139af32a45364794d11f845ab6cf) Thanks [@phryneas](https://github.com/phryneas)! - Add a `resetCache` method to `DocumentTransform` and hook `InMemoryCache.addTypenameTransform` up to `InMemoryCache.gc` + +- [#11367](https://github.com/apollographql/apollo-client/pull/11367) [`30d17bfeb`](https://github.com/apollographql/apollo-client/commit/30d17bfebe44dbfa7b78c8982cfeb49afd37129c) Thanks [@phryneas](https://github.com/phryneas)! - `print`: use `WeakCache` instead of `WeakMap` + +- [#11385](https://github.com/apollographql/apollo-client/pull/11385) [`d9ca4f082`](https://github.com/apollographql/apollo-client/commit/d9ca4f0821c66ae4f03cf35a7ac93fe604cc6de3) Thanks [@phryneas](https://github.com/phryneas)! - ensure `defaultContext` is also used for mutations and subscriptions + +- [#11387](https://github.com/apollographql/apollo-client/pull/11387) [`4dce8673b`](https://github.com/apollographql/apollo-client/commit/4dce8673b1757d8a3a4edd2996d780e86fad14e3) Thanks [@phryneas](https://github.com/phryneas)! - `QueryManager.transformCache`: use `WeakCache` instead of `WeakMap` + +- [#11371](https://github.com/apollographql/apollo-client/pull/11371) [`ebd8fe2c1`](https://github.com/apollographql/apollo-client/commit/ebd8fe2c1b8b50bfeb2da20aeca5671300fb5564) Thanks [@phryneas](https://github.com/phryneas)! - Clarify types of `EntityStore.makeCacheKey`. + +- [#11355](https://github.com/apollographql/apollo-client/pull/11355) [`7d8e18493`](https://github.com/apollographql/apollo-client/commit/7d8e18493cd13134726c6643cbf0fadb08be2d37) Thanks [@phryneas](https://github.com/phryneas)! - InMemoryCache.gc now also triggers FragmentRegistry.resetCaches (if there is a FragmentRegistry) + ## 3.8.8 ### Patch Changes @@ -30,6 +249,124 @@ - [#10931](https://github.com/apollographql/apollo-client/pull/10931) [`e5acf910e`](https://github.com/apollographql/apollo-client/commit/e5acf910e39752b453540b6751046d1c19b66350) Thanks [@phryneas](https://github.com/phryneas)! - `useMutation`: also reset internal state on reset +## 3.9.0-alpha.4 + +### Minor Changes + +- [#11175](https://github.com/apollographql/apollo-client/pull/11175) [`d6d14911c`](https://github.com/apollographql/apollo-client/commit/d6d14911c40782cd6d69167b6f6169c890091ccb) Thanks [@phryneas](https://github.com/phryneas)! - To work around issues in React Server Components, especially with bundling for + the Next.js "edge" runtime we now use an external package to wrap `react` imports + instead of importing React directly. + +### Patch Changes + +- [#11343](https://github.com/apollographql/apollo-client/pull/11343) [`776631de4`](https://github.com/apollographql/apollo-client/commit/776631de4500d56252f6f5fdaf29a81c41dfbdc7) Thanks [@phryneas](https://github.com/phryneas)! - Add `reset` method to `print`, hook up to `InMemoryCache.gc` + +## 3.9.0-alpha.3 + +### Minor Changes + +- [#11301](https://github.com/apollographql/apollo-client/pull/11301) [`46ab032af`](https://github.com/apollographql/apollo-client/commit/46ab032af83a01f184bfcce5edba4b55dbb2962a) Thanks [@alessbell](https://github.com/alessbell)! - Add multipart subscription network adapters for Relay and urql + + ### Relay + + ```tsx + import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/relay"; + import { Environment, Network, RecordSource, Store } from "relay-runtime"; + + const fetchMultipartSubs = createFetchMultipartSubscription( + "http://localhost:4000", + ); + + const network = Network.create(fetchQuery, fetchMultipartSubs); + + export const RelayEnvironment = new Environment({ + network, + store: new Store(new RecordSource()), + }); + ``` + + ### Urql + + ```tsx + import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/urql"; + import { Client, fetchExchange, subscriptionExchange } from "@urql/core"; + + const url = "http://localhost:4000"; + + const multipartSubscriptionForwarder = createFetchMultipartSubscription(url); + + const client = new Client({ + url, + exchanges: [ + fetchExchange, + subscriptionExchange({ + forwardSubscription: multipartSubscriptionForwarder, + }), + ], + }); + ``` + +### Patch Changes + +- [#11275](https://github.com/apollographql/apollo-client/pull/11275) [`3862f9ba9`](https://github.com/apollographql/apollo-client/commit/3862f9ba9086394c4cf4c2ecd99e8e0f6cf44885) Thanks [@phryneas](https://github.com/phryneas)! - Add a `defaultContext` option and property on `ApolloClient`, e.g. for keeping track of changing auth tokens or dependency injection. + + This can be used e.g. in authentication scenarios, where a new token might be + generated outside of the link chain and should passed into the link chain. + + ```js + import { ApolloClient, createHttpLink, InMemoryCache } from "@apollo/client"; + import { setContext } from "@apollo/client/link/context"; + + const httpLink = createHttpLink({ + uri: "/graphql", + }); + + const authLink = setContext((_, { headers, token }) => { + return { + headers: { + ...headers, + authorization: token ? `Bearer ${token}` : "", + }, + }; + }); + + const client = new ApolloClient({ + link: authLink.concat(httpLink), + cache: new InMemoryCache(), + }); + + // somewhere else in your application + function onNewToken(newToken) { + // token can now be changed for future requests without need for a global + // variable, scoped ref or recreating the client + client.defaultContext.token = newToken; + } + ``` + +- [#11297](https://github.com/apollographql/apollo-client/pull/11297) [`c8c76a522`](https://github.com/apollographql/apollo-client/commit/c8c76a522e593de0d06cff73fde2d9e88152bed6) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Add an explicit return type for the `useReadQuery` hook called `UseReadQueryResult`. Previously the return type of this hook was inferred from the return value. + +## 3.9.0-alpha.2 + +### Patch Changes + +- [#11254](https://github.com/apollographql/apollo-client/pull/11254) [`d08970d34`](https://github.com/apollographql/apollo-client/commit/d08970d348cf4ad6d80c6baf85b4a4cd4034a3bb) Thanks [@benjamn](https://github.com/benjamn)! - Decouple `canonicalStringify` from `ObjectCanon` for better time and memory performance. + +## 3.9.0-alpha.1 + +### Minor Changes + +- [#11178](https://github.com/apollographql/apollo-client/pull/11178) [`4d64a6fa2`](https://github.com/apollographql/apollo-client/commit/4d64a6fa2ad5abe6f7f172c164f5e1fc2cb89829) Thanks [@sebakerckhof](https://github.com/sebakerckhof)! - Support re-using of mocks in the MockedProvider + +## 3.9.0-alpha.0 + +### Minor Changes + +- [#11202](https://github.com/apollographql/apollo-client/pull/11202) [`7c2bc08b2`](https://github.com/apollographql/apollo-client/commit/7c2bc08b2ab46b9aa181d187a27aec2ad7129599) Thanks [@benjamn](https://github.com/benjamn)! - Prevent `QueryInfo#markResult` mutation of `result.data` and return cache data consistently whether complete or incomplete. + +- [#6701](https://github.com/apollographql/apollo-client/pull/6701) [`8d2b4e107`](https://github.com/apollographql/apollo-client/commit/8d2b4e107d7c21563894ced3a65d631183b58fd9) Thanks [@prowe](https://github.com/prowe)! - Ability to dynamically match mocks + + Adds support for a new property `MockedResponse.variableMatcher`: a predicate function that accepts a `variables` param. If `true`, the `variables` will be passed into the `ResultFunction` to help dynamically build a response. + ## 3.8.7 ### Patch Changes diff --git a/api-extractor.json b/api-extractor.json index 5a0a74befa3..d026e304ed8 100644 --- a/api-extractor.json +++ b/api-extractor.json @@ -119,17 +119,21 @@ "logLevel": "none" }, + "ae-internal-missing-underscore": { + "logLevel": "none", + "addToApiReportFile": false + }, + "ae-unresolved-link": { "logLevel": "warning", "addToApiReportFile": true } + }, + "tsdocMessageReporting": { + "tsdoc-escape-greater-than": { + "logLevel": "none", + "addToApiReportFile": false + } } - - // "ae-extra-release-tag": { - // "logLevel": "warning", - // "addToApiReportFile": true - // }, - // - // . . . } } diff --git a/config/apiExtractor.ts b/config/apiExtractor.ts index 917986c3da2..b902353a14c 100644 --- a/config/apiExtractor.ts +++ b/config/apiExtractor.ts @@ -6,9 +6,11 @@ import { IConfigFile, } from "@microsoft/api-extractor"; import { parseArgs } from "node:util"; +import fs from "node:fs"; // @ts-ignore -import { map } from "./entryPoints.js"; +import { map, buildDocEntryPoints } from "./entryPoints.js"; +import { readFileSync } from "fs"; const parsed = parseArgs({ options: { @@ -39,37 +41,63 @@ const packageJsonFullPath = path.resolve(__dirname, "../package.json"); process.exitCode = 0; -map((entryPoint: { dirs: string[] }) => { - if (entryPoint.dirs.length > 0 && parsed.values["main-only"]) return; +const tempDir = fs.mkdtempSync("api-model"); +try { + if (parsed.values.generate?.includes("docModel")) { + console.log( + "\n\nCreating API extractor docmodel for the a combination of all entry points" + ); + const entryPointFile = path.join(tempDir, "entry.d.ts"); + fs.writeFileSync(entryPointFile, buildDocEntryPoints()); - const path = entryPoint.dirs.join("/"); - const mainEntryPointFilePath = - `/dist/${path}/index.d.ts`.replace("//", "/"); - console.log( - "\n\nCreating API extractor report for " + mainEntryPointFilePath - ); + buildReport(entryPointFile, "docModel"); + } + + if (parsed.values.generate?.includes("apiReport")) { + map((entryPoint: { dirs: string[] }) => { + const path = entryPoint.dirs.join("/"); + const mainEntryPointFilePath = + `/dist/${path}/index.d.ts`.replace("//", "/"); + console.log( + "\n\nCreating API extractor report for " + mainEntryPointFilePath + ); + buildReport( + mainEntryPointFilePath, + "apiReport", + `api-report${path ? "-" + path.replace(/\//g, "_") : ""}.md` + ); + }); + } +} finally { + fs.rmSync(tempDir, { recursive: true }); +} +function buildReport( + mainEntryPointFilePath: string, + mode: "apiReport" | "docModel", + reportFileName = "" +) { const configObject: IConfigFile = { ...(JSON.parse(JSON.stringify(baseConfig)) as IConfigFile), mainEntryPointFilePath, }; - configObject.apiReport!.reportFileName = `api-report${ - path ? "-" + path.replace("/", "_") : "" - }.md`; - - configObject.apiReport!.enabled = - parsed.values.generate?.includes("apiReport") || false; - - configObject.docModel!.enabled = - parsed.values.generate?.includes("docModel") || false; - - if (entryPoint.dirs.length !== 0) { + if (mode === "apiReport") { + configObject.apiReport!.enabled = true; configObject.docModel = { enabled: false }; - configObject.tsdocMetadata = { enabled: false }; configObject.messages!.extractorMessageReporting![ "ae-unresolved-link" ]!.logLevel = ExtractorLogLevel.None; + configObject.apiReport!.reportFileName = reportFileName; + } else { + configObject.docModel!.enabled = true; + configObject.apiReport = { + enabled: false, + // this has to point to an existing folder, otherwise the extractor will fail + // but it will not write the file + reportFileName: "disabled.md", + reportFolder: tempDir, + }; } const extractorConfig = ExtractorConfig.prepare({ @@ -83,13 +111,36 @@ map((entryPoint: { dirs: string[] }) => { showVerboseMessages: true, }); - if (extractorResult.succeeded) { + let succeededAdditionalChecks = true; + if (fs.existsSync(extractorConfig.reportFilePath)) { + const contents = readFileSync(extractorConfig.reportFilePath, "utf8"); + if (contents.includes("rehackt")) { + succeededAdditionalChecks = false; + console.error( + "❗ %s contains a reference to the `rehackt` package!", + extractorConfig.reportFilePath + ); + } + if (contents.includes('/// ')) { + succeededAdditionalChecks = false; + console.error( + "❗ %s contains a reference to the global `React` type!/n" + + 'Use `import type * as ReactTypes from "react";` instead', + extractorConfig.reportFilePath + ); + } + } + + if (extractorResult.succeeded && succeededAdditionalChecks) { console.log(`✅ API Extractor completed successfully`); } else { console.error( `❗ API Extractor completed with ${extractorResult.errorCount} errors` + ` and ${extractorResult.warningCount} warnings` ); + if (!succeededAdditionalChecks) { + console.error("Additional checks failed."); + } process.exitCode = 1; } -}); +} diff --git a/config/entryPoints.js b/config/entryPoints.js index dbd41ad4d64..cad194d61aa 100644 --- a/config/entryPoints.js +++ b/config/entryPoints.js @@ -22,11 +22,14 @@ const entryPoints = [ { dirs: ["react", "context"] }, { dirs: ["react", "hoc"] }, { dirs: ["react", "hooks"] }, + { dirs: ["react", "internal"] }, { dirs: ["react", "parser"] }, { dirs: ["react", "ssr"] }, { dirs: ["testing"], extensions: [".js", ".jsx"] }, { dirs: ["testing", "core"] }, { dirs: ["utilities"] }, + { dirs: ["utilities", "subscriptions", "relay"] }, + { dirs: ["utilities", "subscriptions", "urql"] }, { dirs: ["utilities", "globals"], sideEffects: true }, ]; @@ -123,3 +126,14 @@ function arraysEqualUpTo(a, b, end) { } return true; } + +exports.buildDocEntryPoints = () => { + const dist = path.resolve(__dirname, "../dist"); + const entryPoints = exports.map((entryPoint) => { + return `export * from "${dist}/${entryPoint.dirs.join("/")}/index.d.ts";`; + }); + entryPoints.push( + `export * from "${dist}/react/types/types.documentation.ts";` + ); + return entryPoints.join("\n"); +}; diff --git a/config/inlineInheritDoc.ts b/config/inlineInheritDoc.ts index 5222dafde7a..704054f28ec 100644 --- a/config/inlineInheritDoc.ts +++ b/config/inlineInheritDoc.ts @@ -23,10 +23,13 @@ */ /** End file docs */ +// @ts-ignore +import { buildDocEntryPoints } from "./entryPoints.js"; // @ts-ignore import { Project, ts, printNode, Node } from "ts-morph"; import { ApiModel, ApiDocumentedItem } from "@microsoft/api-extractor-model"; import { DeclarationReference } from "@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference"; +import { StringBuilder, TSDocEmitter } from "@microsoft/tsdoc"; import fs from "node:fs"; import path from "node:path"; @@ -53,7 +56,12 @@ function getCommentFor(canonicalReference: string) { `Could not resolve canonical reference "${canonicalReference}"` ); if (apiItem instanceof ApiDocumentedItem) { - return apiItem.tsdocComment?.emitAsTsdoc(); + if (!apiItem.tsdocComment) return ""; + const stringBuilder = new StringBuilder(); + const emitter = new TSDocEmitter(); + emitter["_emitCommentFraming"] = false; + emitter["_renderCompleteObject"](stringBuilder, apiItem.tsdocComment); + return stringBuilder.toString(); } else { throw new Error( `"${canonicalReference}" is not documented, so no documentation can be inherited.` @@ -64,6 +72,9 @@ function getCommentFor(canonicalReference: string) { function loadApiModel() { const tempDir = fs.mkdtempSync("api-model"); try { + const entryPointFile = path.join(tempDir, "entry.d.ts"); + fs.writeFileSync(entryPointFile, buildDocEntryPoints()); + // Load and parse the api-extractor.json file const configObjectFullPath = path.resolve( __dirname, @@ -73,6 +84,7 @@ function loadApiModel() { const tempModelFile = path.join(tempDir, "client.api.json"); const configObject = ExtractorConfig.loadFile(configObjectFullPath); + configObject.mainEntryPointFilePath = entryPointFile; configObject.docModel = { ...configObject.docModel, enabled: true, @@ -128,21 +140,32 @@ function processComments() { const sourceFiles = project.addSourceFilesAtPaths("dist/**/*.d.ts"); for (const file of sourceFiles) { file.forEachDescendant((node) => { - if (Node.isPropertySignature(node)) { + if ( + Node.isPropertySignature(node) || + Node.isMethodSignature(node) || + Node.isCallSignatureDeclaration(node) + ) { const docsNode = node.getJsDocs()[0]; if (!docsNode) return; const oldText = docsNode.getInnerText(); - const newText = oldText.replace( - inheritDocRegex, - (_, canonicalReference) => { - return getCommentFor(canonicalReference) || ""; - } - ); + let newText = oldText; + while (inheritDocRegex.test(newText)) { + newText = newText.replace( + inheritDocRegex, + (_, canonicalReference) => { + return getCommentFor(canonicalReference) || ""; + } + ); + } if (oldText !== newText) { - docsNode.replaceWithText(newText); + docsNode.replaceWithText(frameComment(newText)) as any; } } }); file.saveSync(); } } + +function frameComment(text: string) { + return `/**\n * ${text.trim().replace(/\n/g, "\n * ")}\n */`; +} diff --git a/config/jest.config.js b/config/jest.config.js index 3dcd6e6de56..6851e2a6e06 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -31,10 +31,13 @@ const ignoreTSXFiles = ".tsx$"; const react17TestFileIgnoreList = [ ignoreTSFiles, - // For now, we only support useSuspenseQuery with React 18, so no need to test - // it with React 17 + // We only support Suspense with React 18, so don't test suspense hooks with + // React 17 "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/docs/shared/ApiDoc/DocBlock.js b/docs/shared/ApiDoc/DocBlock.js index 333bda75afd..28ee46b305a 100644 --- a/docs/shared/ApiDoc/DocBlock.js +++ b/docs/shared/ApiDoc/DocBlock.js @@ -1,27 +1,26 @@ import PropTypes from "prop-types"; import React from "react"; import { Stack } from "@chakra-ui/react"; -import { mdToReact } from "./mdToReact"; import { useApiDocContext } from "."; +import { useMDXComponents } from "@mdx-js/react"; export function DocBlock({ canonicalReference, summary = true, remarks = false, example = false, - remarkCollapsible = true, - since = true, - deprecated = true, + remarksCollapsible = false, + deprecated = false, + releaseTag = false, }) { return ( - {/** TODO: @since, @deprecated etc. */} {deprecated && } - {since && } + {releaseTag && } {summary && } {remarks && ( )} @@ -35,8 +34,7 @@ DocBlock.propTypes = { summary: PropTypes.bool, remarks: PropTypes.bool, example: PropTypes.bool, - remarkCollapsible: PropTypes.bool, - since: PropTypes.bool, + remarksCollapsible: PropTypes.bool, deprecated: PropTypes.bool, }; @@ -57,17 +55,18 @@ MaybeCollapsible.propTypes = { children: PropTypes.node, }; -/** - * Might still need more work on the Gatsby side to get this to work. - */ export function Deprecated({ canonicalReference, collapsible = false }) { const getItem = useApiDocContext(); const item = getItem(canonicalReference); + const MDX = useMDXComponents(); const value = item.comment?.deprecated; if (!value) return null; return ( - {mdToReact(value)} + +

⚠️ Deprecated

+ {value} +
); } @@ -76,33 +75,15 @@ Deprecated.propTypes = { collapsible: PropTypes.bool, }; -/** - * Might still need more work on the Gatsby side to get this to work. - */ -export function Since({ canonicalReference, collapsible = false }) { - const getItem = useApiDocContext(); - const item = getItem(canonicalReference); - const value = item.comment?.since; - if (!value) return null; - return ( - - Added to Apollo Client in version {value} - - ); -} -Since.propTypes = { - canonicalReference: PropTypes.string.isRequired, - collapsible: PropTypes.bool, -}; - export function Summary({ canonicalReference, collapsible = false }) { const getItem = useApiDocContext(); const item = getItem(canonicalReference); + const MDX = useMDXComponents(); const value = item.comment?.summary; if (!value) return null; return ( - {mdToReact(value)} + {value && {value}} ); } @@ -114,11 +95,12 @@ Summary.propTypes = { export function Remarks({ canonicalReference, collapsible = false }) { const getItem = useApiDocContext(); const item = getItem(canonicalReference); + const MDX = useMDXComponents(); const value = item.comment?.remarks?.replace(/^@remarks/g, ""); if (!value) return null; return ( - {mdToReact(value)} + {value && {value}} ); } @@ -134,12 +116,15 @@ export function Example({ }) { const getItem = useApiDocContext(); const item = getItem(canonicalReference); + const MDX = useMDXComponents(); const value = item.comment?.examples[index]; if (!value) return null; return ( - - {mdToReact(value)} - + <> + + {value && {value}} + + ); } Example.propTypes = { @@ -147,3 +132,30 @@ Example.propTypes = { collapsible: PropTypes.bool, index: PropTypes.number, }; + +export function ReleaseTag({ canonicalReference }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + const MDX = useMDXComponents(); + + if (item.releaseTag === "Public") { + return null; + } + + return ( + + This is in{" "} + + {item.releaseTag.toLowerCase()} stage + {" "} + and is subject to breaking changes. + + ); +} + +ReleaseTag.propTypes = { + canonicalReference: PropTypes.string.isRequired, +}; diff --git a/docs/shared/ApiDoc/EnumDetails.js b/docs/shared/ApiDoc/EnumDetails.js new file mode 100644 index 00000000000..a0f7966f55e --- /dev/null +++ b/docs/shared/ApiDoc/EnumDetails.js @@ -0,0 +1,72 @@ +import { useMDXComponents } from "@mdx-js/react"; + +import PropTypes from "prop-types"; +import React, { useMemo } from "react"; +import { DocBlock, useApiDocContext, ApiDocHeading, SectionHeading } from "."; +import { GridItem, Text } from "@chakra-ui/react"; +import { ResponsiveGrid } from "./ResponsiveGrid"; +import { sortWithCustomOrder } from "./sortWithCustomOrder"; + +export function EnumDetails({ + canonicalReference, + headingLevel, + customOrder = [], +}) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + + const sortedMembers = useMemo( + () => item.members.map(getItem).sort(sortWithCustomOrder(customOrder)), + [item.members, getItem, customOrder] + ); + + return ( + <> + + + + + Enumeration Members + + + + {sortedMembers.map((member) => ( + + + + + ))} + + + ); +} + +EnumDetails.propTypes = { + canonicalReference: PropTypes.string.isRequired, + headingLevel: PropTypes.number.isRequired, + customOrder: PropTypes.arrayOf(PropTypes.string), +}; diff --git a/docs/shared/ApiDoc/Function.js b/docs/shared/ApiDoc/Function.js index 97cb0934ce8..aad711fe8f7 100644 --- a/docs/shared/ApiDoc/Function.js +++ b/docs/shared/ApiDoc/Function.js @@ -1,34 +1,55 @@ import PropTypes from "prop-types"; import React from "react"; -import { ApiDocHeading, DocBlock, ParameterTable, useApiDocContext } from "."; - +import { useMDXComponents } from "@mdx-js/react"; +import { + ApiDocHeading, + SubHeading, + DocBlock, + ParameterTable, + useApiDocContext, + PropertySignatureTable, + SourceLink, + Example, + getInterfaceReference, +} from "."; +import { GridItem } from "@chakra-ui/react"; export function FunctionSignature({ canonicalReference, parameterTypes = false, name = true, arrow = false, + highlight = false, }) { + const MDX = useMDXComponents(); const getItem = useApiDocContext(); const { displayName, parameters, returnType } = getItem(canonicalReference); - return ( - <> - {name ? displayName : ""}( - {parameters - .map((p) => { - let pStr = p.name; - if (p.optional) { - pStr += "?"; - } - if (parameterTypes) { - pStr += ": " + p.type; - } - return pStr; - }) - .join(", ")} - ){arrow ? " =>" : ":"} {returnType} - - ); + let paramSignature = parameters + .map((p) => { + let pStr = p.name; + if (p.optional) { + pStr += "?"; + } + if (parameterTypes) { + pStr += ": " + p.type; + } + return pStr; + }) + .join(",\n "); + + if (paramSignature) { + paramSignature = "\n " + paramSignature + "\n"; + } + + const signature = `${arrow ? "" : "function "}${ + name ? displayName : "" + }(${paramSignature})${arrow ? " =>" : ":"} ${returnType}`; + + return highlight ? + + {signature} + + : signature; } FunctionSignature.propTypes = { @@ -36,29 +57,114 @@ FunctionSignature.propTypes = { parameterTypes: PropTypes.bool, name: PropTypes.bool, arrow: PropTypes.bool, + highlight: PropTypes.bool, +}; + +export function ReturnType({ canonicalReference }) { + const MDX = useMDXComponents(); + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + + const interfaceReference = getInterfaceReference( + item.returnType, + item, + getItem + ); + return ( + <> + {item.comment?.returns} + + {item.returnType} + + {interfaceReference ? +
+ + Show/hide child attributes + + +
+ : null} + + ); +} +ReturnType.propTypes = { + canonicalReference: PropTypes.string.isRequired, }; export function FunctionDetails({ canonicalReference, customParameterOrder, headingLevel, + result, }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); return ( <> - + + Example + + + + )} + + Signature + + + + {item.parameters.length == 0 ? null : ( + <> + + Parameters + + + + )} + {( + result === false || (result === undefined && item.returnType === "void") + ) ? + null + : <> + + Result + + {result || } + } ); } @@ -67,4 +173,5 @@ FunctionDetails.propTypes = { canonicalReference: PropTypes.string.isRequired, headingLevel: PropTypes.number.isRequired, customParameterOrder: PropTypes.arrayOf(PropTypes.string), + result: PropTypes.oneOfType([PropTypes.bool, PropTypes.node]), }; diff --git a/docs/shared/ApiDoc/Heading.js b/docs/shared/ApiDoc/Heading.js index e4a5aa9db69..fea39f5c9f4 100644 --- a/docs/shared/ApiDoc/Heading.js +++ b/docs/shared/ApiDoc/Heading.js @@ -1,67 +1,117 @@ import { useMDXComponents } from "@mdx-js/react"; import PropTypes from "prop-types"; import React from "react"; -import { Box, Heading } from "@chakra-ui/react"; +import { Box, Text } from "@chakra-ui/react"; import { FunctionSignature } from "."; import { useApiDocContext } from "./Context"; -const levels = { - 2: "xl", - 3: "lg", - 4: "md", - 5: "sm", - 6: "xs", +export function Heading({ headingLevel, children, as, minVersion, ...props }) { + const MDX = useMDXComponents(); + let heading = children; + + if (as != undefined && headingLevel != undefined) { + throw new Error( + "Heading: Cannot specify both `as` and `headingLevel` at the same time." + ); + } + const Tag = as ? as : MDX[`h${headingLevel}`]; + + return ( + + {heading} + {minVersion ? + + : null} + + ); +} +Heading.propTypes = { + headingLevel: PropTypes.number, + children: PropTypes.node.isRequired, + id: PropTypes.string, + as: PropTypes.any, + minVersion: PropTypes.string, +}; + +export function SubHeading({ canonicalReference, headingLevel, ...props }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); + + return ( + + ); +} +SubHeading.propTypes = { + ...Heading.propTypes, + canonicalReference: PropTypes.string.isRequired, }; export function ApiDocHeading({ canonicalReference, headingLevel, - link = true, + signature = false, + since = false, + prefix = "", + suffix = "", + ...props }) { const MDX = useMDXComponents(); const getItem = useApiDocContext(); const item = getItem(canonicalReference); - const heading = + let heading = ( - item.kind === "MethodSignature" || - item.kind === "Function" || - item.kind === "Method" + signature && + (item.kind === "MethodSignature" || + item.kind === "Function" || + item.kind === "Method") ) ? - : item.displayName; + : {item.displayName}; + return ( - + - {link ? - - {heading} - - : heading} + {prefix} + {heading} + {suffix} - {item.file && ( - - - ({item.file}) - - - )} ); } ApiDocHeading.propTypes = { canonicalReference: PropTypes.string.isRequired, - headingLevel: PropTypes.number.isRequired, - link: PropTypes.bool, + headingLevel: PropTypes.number, + signature: PropTypes.bool, + since: PropTypes.bool, + prefix: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + suffix: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), }; + +export function SectionHeading(props) { + return ( + + ); +} +SectionHeading.propTypes = Text.propTypes; diff --git a/docs/shared/ApiDoc/InterfaceDetails.js b/docs/shared/ApiDoc/InterfaceDetails.js index e4b439181c4..92c5ae7ae55 100644 --- a/docs/shared/ApiDoc/InterfaceDetails.js +++ b/docs/shared/ApiDoc/InterfaceDetails.js @@ -1,12 +1,21 @@ import PropTypes from "prop-types"; import React from "react"; -import { ApiDocHeading, DocBlock, PropertySignatureTable } from "."; +import { GridItem } from "@chakra-ui/react"; +import { + ApiDocHeading, + DocBlock, + PropertySignatureTable, + useApiDocContext, + SectionHeading, +} from "."; export function InterfaceDetails({ canonicalReference, headingLevel, link, customPropertyOrder, }) { + const getItem = useApiDocContext(); + const item = getItem(canonicalReference); return ( <> - + + + Properties + ); diff --git a/docs/shared/ApiDoc/ParameterTable.js b/docs/shared/ApiDoc/ParameterTable.js index 49f2a7abf9c..44bd51feada 100644 --- a/docs/shared/ApiDoc/ParameterTable.js +++ b/docs/shared/ApiDoc/ParameterTable.js @@ -2,10 +2,14 @@ import { useMDXComponents } from "@mdx-js/react"; import PropTypes from "prop-types"; import React from "react"; -import { GridItem, chakra } from "@chakra-ui/react"; -import { PropertySignatureTable, useApiDocContext } from "."; +import { GridItem, Text } from "@chakra-ui/react"; +import { + PropertySignatureTable, + SectionHeading, + getInterfaceReference, + useApiDocContext, +} from "."; import { ResponsiveGrid } from "./ResponsiveGrid"; -import { mdToReact } from "./mdToReact"; export function ParameterTable({ canonicalReference }) { const MDX = useMDXComponents(); @@ -16,45 +20,34 @@ export function ParameterTable({ canonicalReference }) { return ( <> - - - Parameters - - Name / Type Description {item.parameters.map((parameter) => { - const baseType = parameter.type.split("<")[0]; - const reference = getItem( - item.references?.find((r) => r.text === baseType) - ?.canonicalReference, - false + const interfaceReference = getInterfaceReference( + parameter.type, + item, + getItem ); - const interfaceReference = - reference?.kind === "Interface" ? reference : null; + const id = `${item.displayName.toLowerCase()}-parameters-${parameter.name.toLowerCase()}`; return ( - + - - {parameter.name} - {parameter.optional ? - (optional) - : null} - + + + {parameter.name} + {parameter.optional ? + (optional) + : null} + + {parameter.type} @@ -65,7 +58,9 @@ export function ParameterTable({ canonicalReference }) { lineHeight="base" borderBottom={interfaceReference ? "none" : undefined} > - {mdToReact(parameter.comment)} + {parameter.comment && ( + {parameter.comment} + )} {interfaceReference && (
@@ -75,8 +70,7 @@ export function ParameterTable({ canonicalReference }) {
)} @@ -90,4 +84,5 @@ export function ParameterTable({ canonicalReference }) { ParameterTable.propTypes = { canonicalReference: PropTypes.string.isRequired, + showHeaders: PropTypes.bool, }; diff --git a/docs/shared/ApiDoc/PropertyDetails.js b/docs/shared/ApiDoc/PropertyDetails.js new file mode 100644 index 00000000000..e8c0f28faae --- /dev/null +++ b/docs/shared/ApiDoc/PropertyDetails.js @@ -0,0 +1,27 @@ +import PropTypes from "prop-types"; +import React from "react"; +import { ApiDocHeading, DocBlock } from "."; + +export function PropertyDetails({ canonicalReference, headingLevel }) { + return ( + <> + + + + ); +} + +PropertyDetails.propTypes = { + canonicalReference: PropTypes.string.isRequired, + headingLevel: PropTypes.number.isRequired, +}; diff --git a/docs/shared/ApiDoc/PropertySignatureTable.js b/docs/shared/ApiDoc/PropertySignatureTable.js index 317b2926f0f..40fa53b3df6 100644 --- a/docs/shared/ApiDoc/PropertySignatureTable.js +++ b/docs/shared/ApiDoc/PropertySignatureTable.js @@ -2,56 +2,42 @@ import { useMDXComponents } from "@mdx-js/react"; import PropTypes from "prop-types"; import React, { useMemo } from "react"; -import { DocBlock, FunctionSignature, useApiDocContext } from "."; -import { GridItem, Text, chakra } from "@chakra-ui/react"; +import { + DocBlock, + FunctionSignature, + useApiDocContext, + ApiDocHeading, +} from "."; +import { GridItem, Text } from "@chakra-ui/react"; import { ResponsiveGrid } from "./ResponsiveGrid"; +import { groupItems } from "./sortWithCustomOrder"; export function PropertySignatureTable({ canonicalReference, prefix = "", - showHeaders = true, + showHeaders = false, display = "parent", customOrder = [], + idPrefix = "", }) { const MDX = useMDXComponents(); const getItem = useApiDocContext(); const item = getItem(canonicalReference); - const Wrapper = display === "parent" ? ResponsiveGrid : React.Fragment; - const sortedProperties = useMemo( - () => - item.properties.map(getItem).sort((a, b) => { - const aIndex = customOrder.indexOf(a.displayName); - const bIndex = customOrder.indexOf(b.displayName); - if (aIndex >= 0 && bIndex >= 0) { - return aIndex - bIndex; - } else if (aIndex >= 0) { - return -1; - } else if (bIndex >= 0) { - return 1; - } else { - return a.displayName.localeCompare(b.displayName); - } - }), + const Wrapper = display === "parent" ? ResponsiveGrid : React.Fragment; + const groupedProperties = useMemo( + () => groupItems(item.properties.map(getItem), customOrder), [item.properties, getItem, customOrder] ); + if (item.childrenIncomplete) { + console.warn( + "Warning: some properties might be missing from the table due to complex inheritance!", + item.childrenIncompleteDetails + ); + } return ( <> - {showHeaders ? - - - Properties - - - : null} {item.childrenIncomplete ?
@@ -67,41 +53,65 @@ export function PropertySignatureTable({ Description : null} - - {sortedProperties.map((property) => ( - - - - - - {prefix} - - {property.displayName} - - {property.optional ? - (optional) - : null} - - - {property.kind === "MethodSignature" ? - - : property.type} - - - - - - - ))} + {Object.entries(groupedProperties).map( + ([groupName, sortedProperties]) => ( + <> + {groupName ? + {groupName} + : null} + {sortedProperties.map((property) => ( + + + + {prefix} + + : null + } + suffix={property.optional ? (optional) : null} + link={!!idPrefix} + id={ + idPrefix ? + `${idPrefix}-${property.displayName.toLowerCase()}` + : undefined + } + /> + + {property.kind === "MethodSignature" ? + + : property.type} + + + + + + + ))} + + ) + )} ); @@ -113,4 +123,5 @@ PropertySignatureTable.propTypes = { showHeaders: PropTypes.bool, display: PropTypes.oneOf(["parent", "child"]), customOrder: PropTypes.arrayOf(PropTypes.string), + idPrefix: PropTypes.string, }; diff --git a/docs/shared/ApiDoc/ResponsiveGrid.js b/docs/shared/ApiDoc/ResponsiveGrid.js index 691a4afebcf..2f7b1b93931 100644 --- a/docs/shared/ApiDoc/ResponsiveGrid.js +++ b/docs/shared/ApiDoc/ResponsiveGrid.js @@ -57,7 +57,7 @@ export function ResponsiveGridStyles() { ); } -export function ResponsiveGrid({ children }) { +export function ResponsiveGrid({ children, columns = 2 }) { /* responsiveness not regarding screen width, but actual available space: if less than 350px, show only one column @@ -66,7 +66,11 @@ export function ResponsiveGrid({ children }) { return ( + + ({item.file}) + +
+ : null; +} +SourceLink.propTypes = { + canonicalReference: PropTypes.string.isRequired, +}; diff --git a/docs/shared/ApiDoc/Tuple.js b/docs/shared/ApiDoc/Tuple.js new file mode 100644 index 00000000000..3ab881151b3 --- /dev/null +++ b/docs/shared/ApiDoc/Tuple.js @@ -0,0 +1,68 @@ +import React from "react"; +import { useMDXComponents } from "@mdx-js/react"; +import { useApiDocContext, PropertySignatureTable } from "."; +import PropTypes from "prop-types"; + +export function ManualTuple({ elements = [], idPrefix = "" }) { + const MDX = useMDXComponents(); + const getItem = useApiDocContext(); + + return ( + + + + Name + Type + Description + + + + {elements.map( + ({ name, type, description, canonicalReference }, idx) => { + const item = getItem(canonicalReference); + const separatorStyle = item ? { borderBottom: 0 } : {}; + return ( + + + {name} + + {type} + + {description} + + {item ? + + +
+ Show/hide child attributes + +
+
+
+ : null} +
+ ); + } + )} +
+
+ ); +} +ManualTuple.propTypes = { + elements: PropTypes.arrayOf( + PropTypes.shape({ + name: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + description: PropTypes.oneOfType([PropTypes.node, PropTypes.string]) + .isRequired, + canonicalReference: PropTypes.string, + }) + ).isRequired, +}; diff --git a/docs/shared/ApiDoc/getInterfaceReference.js b/docs/shared/ApiDoc/getInterfaceReference.js new file mode 100644 index 00000000000..b9b9202ae92 --- /dev/null +++ b/docs/shared/ApiDoc/getInterfaceReference.js @@ -0,0 +1,8 @@ +export function getInterfaceReference(type, item, getItem) { + const baseType = type.replace(/\b(Partial|Omit|Promise) r.text === baseType)?.canonicalReference, + false + ); + return reference?.kind === "Interface" ? reference : null; +} diff --git a/docs/shared/ApiDoc/index.js b/docs/shared/ApiDoc/index.js index 66bfa413afa..06728b10116 100644 --- a/docs/shared/ApiDoc/index.js +++ b/docs/shared/ApiDoc/index.js @@ -4,11 +4,16 @@ export { Deprecated, Example, Remarks, - Since, + ReleaseTag, Summary, } from "./DocBlock"; export { PropertySignatureTable } from "./PropertySignatureTable"; -export { ApiDocHeading } from "./Heading"; +export { ApiDocHeading, SubHeading, SectionHeading } from "./Heading"; export { InterfaceDetails } from "./InterfaceDetails"; export { FunctionSignature, FunctionDetails } from "./Function"; export { ParameterTable } from "./ParameterTable"; +export { PropertyDetails } from "./PropertyDetails"; +export { EnumDetails } from "./EnumDetails"; +export { ManualTuple } from "./Tuple"; +export { getInterfaceReference } from "./getInterfaceReference"; +export { SourceLink } from "./SourceLink"; diff --git a/docs/shared/ApiDoc/mdToReact.js b/docs/shared/ApiDoc/mdToReact.js deleted file mode 100644 index 307ca38c7bf..00000000000 --- a/docs/shared/ApiDoc/mdToReact.js +++ /dev/null @@ -1,20 +0,0 @@ -import PropTypes from "prop-types"; -import React from "react"; -import ReactMarkdown from "react-markdown"; -import { useMDXComponents } from "@mdx-js/react"; - -export function mdToReact(text) { - const sanitized = text - .replace(/\{@link (\w*)\}/g, "[$1](#$1)") - .replace(//g, ""); - return ; -} - -function RenderMd({ markdown }) { - return ( - {markdown} - ); -} -RenderMd.propTypes = { - markdown: PropTypes.string.isRequired, -}; diff --git a/docs/shared/ApiDoc/sortWithCustomOrder.js b/docs/shared/ApiDoc/sortWithCustomOrder.js new file mode 100644 index 00000000000..c2cd411e304 --- /dev/null +++ b/docs/shared/ApiDoc/sortWithCustomOrder.js @@ -0,0 +1,71 @@ +/** + * Sorts items by their `displayName` with a custom order: + * - items within the `customOrder` array will be sorted to the start, + * sorted by the order of the `customOrder` array + * - items not in the `customOrder` array will be sorted in lexicographical order after that + * - deprecated items will be sorted in lexicographical order to the end + */ +export function sortWithCustomOrder(customOrder = []) { + return (a, b) => { + let aIndex = customOrder.indexOf(a.displayName); + if (aIndex == -1) { + aIndex = + a.comment?.deprecated ? + Number.MAX_SAFE_INTEGER + : Number.MAX_SAFE_INTEGER - 1; + } + let bIndex = customOrder.indexOf(b.displayName); + if (bIndex == -1) { + bIndex = + b.comment?.deprecated ? + Number.MAX_SAFE_INTEGER + : Number.MAX_SAFE_INTEGER - 1; + } + if (aIndex === bIndex) { + return sortLocally(a.displayName, b.displayName); + } else { + return aIndex - bIndex; + } + }; +} + +function sortLocally(a, b) { + return a.localeCompare(b); +} + +/** + * + * @param {Array<{displayName: string, comment: { docGroup: string }}>} items + * @param {string[]} customOrder + */ +export function groupItems(items = [], customOrder = []) { + const customItems = []; + const groupedItems = []; + for (const item of items) { + if (customOrder.includes(item.displayName)) customItems.push(item); + else groupedItems.push(item); + } + customItems.sort(sortWithCustomOrder(customOrder)); + const groupNames = [ + ...new Set(groupedItems.map((item) => item.comment?.docGroup || "Other")), + ].sort(sortLocally); + const groups = Object.fromEntries(groupNames.map((name) => [name, []])); + for (const item of groupedItems) { + groups[item.comment?.docGroup || "Other"].push(item); + } + for (const group of Object.values(groups)) { + group.sort(sortWithCustomOrder([])); + } + const groupsWithoutPrefix = Object.fromEntries( + Object.entries(groups).map(([name, items]) => [ + name.replace(/^\s*\d*\.\s*/, ""), + items, + ]) + ); + return customItems.length === 0 ? + groupsWithoutPrefix + : { + "": customItems, + ...groupsWithoutPrefix, + }; +} diff --git a/docs/shared/Overrides/UseLoadableQueryResult.js b/docs/shared/Overrides/UseLoadableQueryResult.js new file mode 100644 index 00000000000..19a4d345d44 --- /dev/null +++ b/docs/shared/Overrides/UseLoadableQueryResult.js @@ -0,0 +1,58 @@ +import React from "react"; +import { useMDXComponents } from "@mdx-js/react"; +import { ManualTuple } from "../ApiDoc"; + +const HANDLERS = `{ + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; +}`; + +const RETURN_VALUE = `[ + loadQuery: LoadQueryFunction, + queryRef: QueryReference | null, + { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; + reset: ResetFunction; + } +]`; + +export function UseLoadableQueryResult() { + const MDX = useMDXComponents(); + + return ( +
+ + {RETURN_VALUE} + + A tuple of three values: + ", + description: + "A function used to imperatively load a query. Calling this function will create or update the `queryRef` returned by `useLoadableQuery`, which should be passed to `useReadQuery`.", + }, + { + name: "queryRef", + type: "QueryReference | null", + description: + "The `queryRef` used by `useReadQuery` to read the query result.", + canonicalReference: "@apollo/client!QueryReference:interface", + }, + { + name: "handlers", + description: + "Additional handlers used for the query, such as `refetch`.", + type: HANDLERS, + }, + ]} + /> +
+ ); +} + +UseLoadableQueryResult.propTypes = {}; diff --git a/docs/shared/apollo-provider.mdx b/docs/shared/apollo-provider.mdx deleted file mode 100644 index 8b137891791..00000000000 --- a/docs/shared/apollo-provider.mdx +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/shared/document-transform-options.mdx b/docs/shared/document-transform-options.mdx deleted file mode 100644 index 3675c19cfdf..00000000000 --- a/docs/shared/document-transform-options.mdx +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -###### `getCacheKey` - -`(document: DocumentNode) => any[] | undefined` - - -Defines a custom cache key for a GraphQL document that will determine whether to re-run the document transform when given the same input GraphQL document. Returns an array that defines the cache key. Return `undefined` to disable caching for that GraphQL document. - -> **Note:** The items in the array may be any type, but also need to be referentially stable to guarantee a stable cache key. - -The default implementation of this function returns the `document` as the cache key. -
- -###### `cache` - -`boolean` - - -Determines whether to cache the transformed GraphQL document. Caching can speed up repeated calls to the document transform for the same input document. Set to `false` to completely disable caching for the document transform. When disabled, this option takes precedence over the [`getCacheKey`](#getcachekey) option. - -The default value is `true`. -
- diff --git a/docs/shared/mutation-options.mdx b/docs/shared/mutation-options.mdx deleted file mode 100644 index fa22a6e6010..00000000000 --- a/docs/shared/mutation-options.mdx +++ /dev/null @@ -1,298 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -**Operation options** - -
- -###### `mutation` - -`DocumentNode` - - -A GraphQL query string parsed into an AST with the `gql` template literal. - -**Optional** for the `useMutation` hook, because the mutation can also be provided as the first parameter to the hook. - -**Required** for the `Mutation` component. -
- -###### `variables` - -`{ [key: string]: any }` - - -An object containing all of the GraphQL variables your mutation requires to execute. - -Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. - -
- -###### `errorPolicy` - -`ErrorPolicy` - - -Specifies how the mutation handles a response that returns both GraphQL errors and partial results. - -For details, see [GraphQL error policies](/react/data/error-handling/#graphql-error-policies). - -The default value is `none`, meaning that the mutation result includes error details but _not_ partial results. - -
- -###### `onCompleted` - -`(data?: TData, clientOptions?: BaseMutationOptions) => void` - - -A callback function that's called when your mutation successfully completes with zero errors (or if `errorPolicy` is `ignore` and partial data is returned). - -This function is passed the mutation's result `data` and any options passed to the mutation. - -
- -###### `onError` - -`(error: ApolloError, clientOptions?: BaseMutationOptions) => void` - - -A callback function that's called when the mutation encounters one or more errors (unless `errorPolicy` is `ignore`). - -This function is passed an [`ApolloError`](https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/errors/index.ts#L36-L39) object that contains either a `networkError` object or a `graphQLErrors` array, depending on the error(s) that occurred, as well as any options passed the mutation. - -
- -###### `onQueryUpdated` - -`(observableQuery: ObservableQuery, diff: Cache.DiffResult, lastDiff: Cache.DiffResult | undefined) => boolean | TResult` - - - -Optional callback for intercepting queries whose cache data has been updated by the mutation, as well as any queries specified in the [`refetchQueries: [...]`](#refetchQueries) list passed to `client.mutate`. - -Returning a `Promise` from `onQueryUpdated` will cause the final mutation `Promise` to await the returned `Promise`. Returning `false` causes the query to be ignored. - -
- -###### `refetchQueries` - -`Array | ((mutationResult: FetchResult) => Array)` - - -An array (or a function that _returns_ an array) that specifies which queries you want to refetch after the mutation occurs. - -Each array value can be either: - -* An object containing the `query` to execute, along with any `variables` -* A string indicating the operation name of the query to refetch - -
- -###### `awaitRefetchQueries` - -`boolean` - - -If `true`, makes sure all queries included in `refetchQueries` are completed before the mutation is considered complete. - -The default value is `false` (queries are refetched asynchronously). - -
- -###### `ignoreResults` - -`boolean` - - -If `true`, the mutation's `data` property is not updated with the mutation's result. - -The default value is `false`. - -
- -**Networking options** - -
- -###### `notifyOnNetworkStatusChange` - -`boolean` - - -If `true`, the in-progress mutation's associated component re-renders whenever the network status changes or a network error occurs. - -The default value is `false`. - -
- -###### `client` - -`ApolloClient` - - -The instance of `ApolloClient` to use to execute the mutation. - -By default, the instance that's passed down via context is used, but you can provide a different instance here. - -
- -###### `context` - -`Record` - - -If you're using [Apollo Link](/react/api/link/introduction/), this object is the initial value of the `context` object that's passed along your link chain. - -
- -**Caching options** - -
- -###### `update` - -`(cache: ApolloCache, mutationResult: FetchResult) => void` - - -A function used to update the Apollo Client cache after the mutation completes. - -For more information, see [Updating the cache after a mutation](/react/data/mutations#updating-the-cache-after-a-mutation). - -
- -###### `optimisticResponse` - -`Object` - - -If provided, Apollo Client caches this temporary (and potentially incorrect) response until the mutation completes, enabling more responsive UI updates. - -For more information, see [Optimistic mutation results](/react/performance/optimistic-ui/). - -
- -###### `fetchPolicy` - -`MutationFetchPolicy` - - -Provide `no-cache` if the mutation's result should _not_ be written to the Apollo Client cache. - -The default value is `network-only` (which means the result _is_ written to the cache). - -Unlike queries, mutations _do not_ support [fetch policies](/react/data/queries/#setting-a-fetch-policy) besides `network-only` and `no-cache`. - -
diff --git a/docs/shared/mutation-result.mdx b/docs/shared/mutation-result.mdx deleted file mode 100644 index 21e4e858137..00000000000 --- a/docs/shared/mutation-result.mdx +++ /dev/null @@ -1,145 +0,0 @@ -**Mutate function:** - - - - - - - - - - - - - - - -
Name /
Type
Description
- -###### `mutate` - -`(options?: MutationOptions) => Promise` - - -A function to trigger the mutation from your UI. You can optionally pass this function any of the following options: - -* `awaitRefetchQueries` -* `context` -* `fetchPolicy` -* `onCompleted` -* `onError` -* `optimisticResponse` -* `refetchQueries` -* `onQueryUpdated` -* `update` -* `variables` -* `client` - -Any option you pass here overrides any existing value for that option that you passed to `useMutation`. - -The mutate function returns a promise that fulfills with your mutation result. -
- -**Mutation result:** - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -###### `data` - -`TData` - - -The data returned from your mutation. Can be `undefined` if `ignoreResults` is `true`. -
- -###### `loading` - -`boolean` - - -If `true`, the mutation is currently in flight. -
- -###### `error` - -`ApolloError` - - -If the mutation produces one or more errors, this object contains either an array of `graphQLErrors` or a single `networkError`. Otherwise, this value is `undefined`. - -For more information, see [Handling operation errors](/react/data/error-handling/). - -
- -###### `called` - -`boolean` - - -If `true`, the mutation's mutate function has been called. - -
- -###### `client` - -`ApolloClient` - - -The instance of Apollo Client that executed the mutation. - -Can be useful for manually executing followup operations or writing data to the cache. - -
- -###### `reset` - -`() => void` - - -A function that you can call to reset the mutation's result to its initial, uncalled state. - -
diff --git a/docs/shared/query-options.mdx b/docs/shared/query-options.mdx deleted file mode 100644 index 2a265ada2da..00000000000 --- a/docs/shared/query-options.mdx +++ /dev/null @@ -1,304 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -**Operation options** - -
- -###### `query` - -`DocumentNode` - - -A GraphQL query string parsed into an AST with the `gql` template literal. - -**Optional** for the `useQuery` hook, because the query can be provided as the first parameter to the hook. **Required** for the `Query` component. -
- -###### `variables` - -`{ [key: string]: any }` - - -An object containing all of the GraphQL variables your query requires to execute. - -Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. - -
- -###### `errorPolicy` - -`ErrorPolicy` - - -Specifies how the query handles a response that returns both GraphQL errors and partial results. - -For details, see [GraphQL error policies](/react/data/error-handling/#graphql-error-policies). - -The default value is `none`, meaning that the query result includes error details but _not_ partial results. - -
- -###### `onCompleted` - -`(data: TData | {}) => void` - - -A callback function that's called when your query successfully completes with zero errors (or if `errorPolicy` is `ignore` and partial data is returned). - -This function is passed the query's result `data`. - -
- -###### `onError` - -`(error: ApolloError) => void` - - -A callback function that's called when the query encounters one or more errors (unless `errorPolicy` is `ignore`). - -This function is passed an [`ApolloError`](https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/errors/index.ts#L36-L39) object that contains either a `networkError` object or a `graphQLErrors` array, depending on the error(s) that occurred. - -
- -###### `skip` - -`boolean` - - -If `true`, the query is _not_ executed. **Not available with `useLazyQuery`.** - -This property is part of Apollo Client's React integration, and it is _not_ available in the [core `ApolloClient` API](/react/api/core/ApolloClient/). - -The default value is `false`. - -
- -**Networking options** - -
- -###### `pollInterval` - -`number` - - -Specifies the interval (in milliseconds) at which the query polls for updated results. - -The default value is `0` (no polling). - -
- -###### `notifyOnNetworkStatusChange` - -`boolean` - - -If `true`, the in-progress query's associated component re-renders whenever the network status changes or a network error occurs. - -The default value is `false`. - -
- -###### `context` - -`Record` - - -If you're using [Apollo Link](/react/api/link/introduction/), this object is the initial value of the `context` object that's passed along your link chain. - -
- -###### `ssr` - -`boolean` - - -Pass `false` to skip executing the query during [server-side rendering](/react/performance/server-side-rendering/). - -
- -###### `client` - -`ApolloClient` - - -The instance of `ApolloClient` to use to execute the query. - -By default, the instance that's passed down via context is used, but you can provide a different instance here. - -
- -**Caching options** - -
- -###### `fetchPolicy` - -`FetchPolicy` - - -Specifies how the query interacts with the Apollo Client cache during execution (for example, whether it checks the cache for results before sending a request to the server). - -For details, see [Setting a fetch policy](/react/data/queries/#setting-a-fetch-policy). - -The default value is `cache-first`. - -
- -###### `nextFetchPolicy` - -`FetchPolicy` - - -Specifies the [`fetchPolicy`](#fetchpolicy) to use for all executions of this query _after_ this execution. - -For example, you can use this to switch back to a `cache-first` fetch policy after using `cache-and-network` or `network-only` for a single execution. - -
- -###### `returnPartialData` - -`boolean` - - -If `true`, the query can return _partial_ results from the cache if the cache doesn't contain results for _all_ queried fields. - -The default value is `false`. - -
- -**Deprecated options** - -
- -###### `partialRefetch` - -`boolean` - - -**Deprecated.** If `true`, causes a query `refetch` if the query result is detected as partial. Setting this option is unnecessary in Apollo Client 3, thanks to a more consistent application of fetch policies. It might be removed in a future release. - -The default value is `false`. - -
diff --git a/docs/shared/query-result.mdx b/docs/shared/query-result.mdx deleted file mode 100644 index aed5e2f478f..00000000000 --- a/docs/shared/query-result.mdx +++ /dev/null @@ -1,275 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -**Operation data** - -
- -###### `data` - -`TData` - - -An object containing the result of your GraphQL query after it completes. - -This value might be `undefined` if a query results in one or more errors (depending on the query's `errorPolicy`). - -
- -###### `previousData` - -`TData` - - -An object containing the result from the most recent _previous_ execution of this query. - -This value is `undefined` if this is the query's first execution. - -
- -###### `error` - -`ApolloError` - - -If the query produces one or more errors, this object contains either an array of `graphQLErrors` or a single `networkError`. Otherwise, this value is `undefined`. - -For more information, see [Handling operation errors](/react/data/error-handling/). - -
- -###### `variables` - -`{ [key: string]: any }` - - -An object containing the variables that were provided for the query. - -
- -**Network info** - -
- -###### `loading` - -`boolean` - - -If `true`, the query is still in flight and results have not yet been returned. - -
- -###### `networkStatus` - -`NetworkStatus` - - -A number indicating the current network state of the query's associated request. [See possible values.](https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/core/networkStatus.ts#L4) - -Used in conjunction with the [`notifyOnNetworkStatusChange`](#notifyonnetworkstatuschange) option. - -
- -###### `client` - -`ApolloClient` - - -The instance of Apollo Client that executed the query. - -Can be useful for manually executing followup queries or writing data to the cache. - -
- -###### `called` - -`boolean` - - -If `true`, the associated lazy query has been executed. - -This field is only present on the result object returned by [`useLazyQuery`](/react/data/queries/#executing-queries-manually). - -
- -**Helper functions** - -
- -###### `refetch` - -`(variables?: Partial) => Promise` - - -A function that enables you to re-execute the query, optionally passing in new `variables`. - -To guarantee that the refetch performs a network request, its `fetchPolicy` is set to `network-only` (unless the original query's `fetchPolicy` is `no-cache` or `cache-and-network`, which also guarantee a network request). - -See also [Refetching](/react/data/queries/#refetching). - -
- -###### `fetchMore` - -`({ query?: DocumentNode, variables?: TVariables, updateQuery: Function}) => Promise` - - -A function that helps you fetch the next set of results for a [paginated list field](/react/pagination/core-api/). - -
- -###### `startPolling` - -`(interval: number) => void` - - -A function that instructs the query to begin re-executing at a specified interval (in milliseconds). - -
- -###### `stopPolling` - -`() => void` - - -A function that instructs the query to stop polling after a previous call to `startPolling`. - -
- -###### `subscribeToMore` - -`(options: { document: DocumentNode, variables?: TVariables, updateQuery?: Function, onError?: Function}) => () => void` - - -A function that enables you to execute a [subscription](/react/data/subscriptions/), usually to subscribe to specific fields that were included in the query. - -This function returns _another_ function that you can call to terminate the subscription. - -
- -###### `updateQuery` - -`(mapFn: (previousResult: TData, options: { variables: TVariables }) => TData) => void` - - -A function that enables you to update the query's cached result without executing a followup GraphQL operation. -See [using updateQuery and updateFragment](/react/caching/cache-interaction/#using-updatequery-and-updatefragment) for additional information. -
diff --git a/docs/shared/subscription-options.mdx b/docs/shared/subscription-options.mdx deleted file mode 100644 index 00148fa2fce..00000000000 --- a/docs/shared/subscription-options.mdx +++ /dev/null @@ -1,14 +0,0 @@ -| Option | Type | Description | -| - | - | - | -| `subscription` | DocumentNode | A GraphQL subscription document parsed into an AST by `graphql-tag`. **Optional** for the `useSubscription` Hook since the subscription can be passed in as the first parameter to the Hook. **Required** for the `Subscription` component. | -| `variables` | { [key: string]: any } | An object containing all of the variables your subscription needs to execute | -| `shouldResubscribe` | boolean | Determines if your subscription should be unsubscribed and subscribed again when an input to the hook (such as `subscription` or `variables`) changes. | -| `skip` | boolean | Determines if the current subscription should be skipped. Useful if, for example, variables depend on previous queries and are not ready yet. | -| `onSubscriptionData` | **Deprecated.** (options: OnSubscriptionDataOptions<TData>) => any | Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives data. The callback `options` object param consists of the current Apollo Client instance in `client`, and the received subscription data in `subscriptionData`. | -| `onData` | (options: OnDataOptions<TData>) => any | Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives data. The callback `options` object param consists of the current Apollo Client instance in `client`, and the received subscription data in `data`. | -| `onError` | (error: ApolloError) => void | Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives an error. | -| `onSubscriptionComplete` | **Deprecated.** () => void | Allows the registration of a callback function that will be triggered when the `useSubscription` Hook / `Subscription` component completes the subscription. | -| `onComplete` | () => void | Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component completes the subscription. | -| `fetchPolicy` | FetchPolicy | How you want your component to interact with the Apollo cache. For details, see [Setting a fetch policy](/react/data/queries/#setting-a-fetch-policy). | -| `context` | Record<string, any> | Shared context between your component and your network interface (Apollo Link). | -| `client` | ApolloClient | An `ApolloClient` instance. By default `useSubscription` / `Subscription` uses the client passed down via context, but a different client can be passed in. | diff --git a/docs/shared/subscription-result.mdx b/docs/shared/subscription-result.mdx deleted file mode 100644 index be7d5295613..00000000000 --- a/docs/shared/subscription-result.mdx +++ /dev/null @@ -1,5 +0,0 @@ -| Property | Type | Description | -| - | - | - | -| `data` | TData | An object containing the result of your GraphQL subscription. Defaults to an empty object. | -| `loading` | boolean | A boolean that indicates whether any initial data has been returned | -| `error` | ApolloError | A runtime error with `graphQLErrors` and `networkError` properties | diff --git a/docs/shared/useBackgroundQuery-options.mdx b/docs/shared/useBackgroundQuery-options.mdx index eb0157d6fdd..d00042a9c98 100644 --- a/docs/shared/useBackgroundQuery-options.mdx +++ b/docs/shared/useBackgroundQuery-options.mdx @@ -89,6 +89,10 @@ If you're using [Apollo Link](/react/api/link/introduction/), this object is the +> **⚠️ Deprecated**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. The default value is `false`. diff --git a/docs/shared/useFragment-options.mdx b/docs/shared/useFragment-options.mdx index e3ca1c63e3b..75a9be37b48 100644 --- a/docs/shared/useFragment-options.mdx +++ b/docs/shared/useFragment-options.mdx @@ -108,6 +108,10 @@ Each key in the object corresponds to a variable name, and that key's value corr +> **⚠️ Deprecated**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. The default value is `false`. diff --git a/docs/shared/useSuspenseQuery-options.mdx b/docs/shared/useSuspenseQuery-options.mdx deleted file mode 100644 index c87dc25c544..00000000000 --- a/docs/shared/useSuspenseQuery-options.mdx +++ /dev/null @@ -1,213 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Name /
Type
Description
- -**Operation options** - -
- -###### `variables` - -`{ [key: string]: any }` - - -An object containing all of the GraphQL variables your query requires to execute. - -Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. - -
- -###### `errorPolicy` - -`ErrorPolicy` - - -Specifies how the query handles a response that returns both GraphQL errors and partial results. - -For details, see [GraphQL error policies](/react/data/error-handling/#graphql-error-policies). - -The default value is `none`, which causes the hook to throw the error. -
- -**Networking options** - -
- -###### `context` - -`Record` - - -If you're using [Apollo Link](/react/api/link/introduction/), this object is the initial value of the `context` object that's passed along your link chain. - -
- -###### `canonizeResults` - -`Boolean` - - -If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. - -The default value is `false`. - -
- -###### `client` - -`ApolloClient` - - -The instance of `ApolloClient` to use to execute the query. - -By default, the instance that's passed down via context is used, but you can provide a different instance here. - -
- -###### `queryKey` - -`string | number | any[]` - - -A unique identifier for the query. Each item in the array must be a stable identifier to prevent infinite fetches. - -This is useful when using the same query and variables combination in more than one component, otherwise the components may clobber each other. This can also be used to force the query to re-evaluate fresh. - -
- -**Caching options** - -
- -###### `fetchPolicy` - -`SuspenseQueryHookFetchPolicy` - - -Specifies how the query interacts with the Apollo Client cache during execution (for example, whether it checks the cache for results before sending a request to the server). - -For details, see [Setting a fetch -policy](/react/data/queries/#setting-a-fetch-policy). This hook only supports -the `cache-first`, `network-only`, `no-cache`, and `cache-and-network` fetch -policies. - -The default value is `cache-first`. - -
- -###### `returnPartialData` - -`boolean` - - -If `true`, the query can return _partial_ results from the cache if the cache doesn't contain results for _all_ queried fields. - -The default value is `false`. - -
- -###### `refetchWritePolicy` - -`"merge" | "overwrite"` - - -Watched queries must opt into overwriting existing data on refetch, by passing `refetchWritePolicy: "overwrite"` in their `WatchQueryOptions`. - -The default value is `"overwrite"`. - -
- -###### `skip` (deprecated) - -`boolean` - - -If `true`, the query is not executed. The default value is `false`. - -This option is deprecated and only supported to ease the migration from `useQuery`. It will be removed in a future release. -Please use [`skipToken`](/react/api/react/hooks#skiptoken`) instead of the `skip` option as it is more type-safe. - -
diff --git a/docs/source/api/cache/InMemoryCache.mdx b/docs/source/api/cache/InMemoryCache.mdx index 2e33c0a3348..7bcc362d9ea 100644 --- a/docs/source/api/cache/InMemoryCache.mdx +++ b/docs/source/api/cache/InMemoryCache.mdx @@ -140,6 +140,10 @@ By specifying the ID of another cached object, you can query arbitrary cached da +> **⚠️ Deprecated**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. The default value is `false`. @@ -591,6 +595,10 @@ A map of any GraphQL variable names and values required by `fragment`. +> **⚠️ Deprecated**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + If `true`, result objects read from the cache will be _canonized_, which means deeply-equal objects will also be `===` (literally the same object), allowing much more efficient comparison of past/present results. The default value is `false`. diff --git a/docs/source/api/core/ObservableQuery.mdx b/docs/source/api/core/ObservableQuery.mdx index 90d4450dfe6..806091f8b28 100644 --- a/docs/source/api/core/ObservableQuery.mdx +++ b/docs/source/api/core/ObservableQuery.mdx @@ -1,26 +1,30 @@ --- title: ObservableQuery description: API reference +api_doc: + - "@apollo/client!ObservableQuery:class" + - "@apollo/client!ApolloQueryResult:interface" + - "@apollo/client!NetworkStatus:enum" --- +import { InterfaceDetails, FunctionDetails, PropertyDetails, EnumDetails } from '../../../shared/ApiDoc'; + ## `ObservableQuery` functions `ApolloClient` Observables extend the Observables implementation provided by [`zen-observable`](https://github.com/zenparsing/zen-observable). Refer to the `zen-observable` documentation for additional context and API options. - - - - - - - - - - - + + + + + + + + + + -## Types - - - +## Types + + diff --git a/docs/source/api/react/components.mdx b/docs/source/api/react/components.mdx index 629d1475c8d..7322b162234 100644 --- a/docs/source/api/react/components.mdx +++ b/docs/source/api/react/components.mdx @@ -1,14 +1,16 @@ --- title: Components description: Deprecated React Apollo render prop component API +api_doc: + - "@apollo/client!QueryFunctionOptions:interface" + - "@apollo/client!QueryResult:interface" + - "@apollo/client!MutationFunctionOptions:interface" + - "@apollo/client!MutationResult:interface" + - "@apollo/client!SubscriptionComponentOptions:interface" + - "@apollo/client!SubscriptionResult:interface" --- -import QueryOptions3 from '../../../shared/query-options.mdx'; -import QueryResult3 from '../../../shared/query-result.mdx'; -import MutationOptions3 from '../../../shared/mutation-options.mdx'; -import MutationResult3 from '../../../shared/mutation-result.mdx'; -import SubscriptionOptions3 from '../../../shared/subscription-options.mdx'; -import SubscriptionResult3 from '../../../shared/subscription-result.mdx'; +import { PropertySignatureTable } from '../../../shared/ApiDoc'; > **Note:** Official support for React Apollo render prop components ended in March 2020. This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. @@ -28,25 +30,25 @@ You then import the library's symbols from `@apollo/client/react/components`. The `Query` component accepts the following props. `query` is **required**. - + ### Render prop function The render prop function that you pass to the `children` prop of `Query` is called with an object (`QueryResult`) that has the following properties. This object contains your query result, plus some helpful functions for refetching, dynamic polling, and pagination. - + ## `Mutation` The Mutation component accepts the following props. Only `mutation` is **required**. - + ### Render prop function The render prop function that you pass to the `children` prop of `Mutation` is called with the `mutate` function and an object with the mutation result. The `mutate` function is how you trigger the mutation from your UI. The object contains your mutation result, plus loading and error state. - + ## `Subscription` @@ -54,10 +56,10 @@ The render prop function that you pass to the `children` prop of `Mutation` is c The Subscription component accepts the following props. Only `subscription` is **required**. - + ### Render prop function -The render prop function that you pass to the `children` prop of `Subscription` is called with an object that has the following properties. + diff --git a/docs/source/api/react/hooks.mdx b/docs/source/api/react/hooks.mdx index 9466e50f24d..80ef1dc3710 100644 --- a/docs/source/api/react/hooks.mdx +++ b/docs/source/api/react/hooks.mdx @@ -1,25 +1,27 @@ --- title: Hooks description: Apollo Client react hooks API reference +minVersion: 3.0.0 +api_doc: + - "@apollo/client!SuspenseQueryHookOptions:interface" + - "@apollo/client!useQuery:function(1)" + - "@apollo/client!useLazyQuery:function(1)" + - "@apollo/client!useMutation:function(1)" + - "@apollo/client!useSubscription:function(1)" + - "@apollo/client!useApolloClient:function(1)" + - "@apollo/client!useReactiveVar:function(1)" + - "@apollo/client!useLoadableQuery:function(5)" + - "@apollo/client!useQueryRefHandlers:function(1)" --- -import QueryOptions3 from '../../../shared/query-options.mdx'; -import QueryResult3 from '../../../shared/query-result.mdx'; -import MutationOptions3 from '../../../shared/mutation-options.mdx'; -import MutationResult3 from '../../../shared/mutation-result.mdx'; -import SubscriptionOptions3 from '../../../shared/subscription-options.mdx'; -import SubscriptionResult3 from '../../../shared/subscription-result.mdx'; import UseFragmentOptions from '../../../shared/useFragment-options.mdx'; import UseFragmentResult from '../../../shared/useFragment-result.mdx'; -import UseSuspenseQueryOptions from '../../../shared/useSuspenseQuery-options.mdx'; import UseBackgroundQueryOptions from '../../../shared/useBackgroundQuery-options.mdx'; import UseSuspenseQueryResult from '../../../shared/useSuspenseQuery-result.mdx'; import UseBackgroundQueryResult from '../../../shared/useBackgroundQuery-result.mdx'; import UseReadQueryResult from '../../../shared/useReadQuery-result.mdx'; - -## Installation - -Apollo Client >= 3 includes React hooks functionality out of the box. You don't need to install any additional packages. +import { FunctionDetails, PropertySignatureTable, ManualTuple, InterfaceDetails } from '../../../shared/ApiDoc'; +import { UseLoadableQueryResult } from '../../../shared/Overrides/UseLoadableQueryResult' ## The `ApolloProvider` component @@ -69,350 +71,93 @@ function WithApolloClient() { } ``` -## `useQuery` - -### Example - -```jsx -import { gql, useQuery } from '@apollo/client'; - -const GET_GREETING = gql` - query GetGreeting($language: String!) { - greeting(language: $language) { - message - } - } -`; - -function Hello() { - const { loading, error, data } = useQuery(GET_GREETING, { - variables: { language: 'english' }, - }); - if (loading) return

Loading ...

; - return

Hello {data.greeting.message}!

; -} -``` - -> Refer to the [Queries](../../data/queries/) section for a more in-depth overview of `useQuery`. - -### Signature - -```ts -function useQuery( - query: DocumentNode, - options?: QueryHookOptions, -): QueryResult {} -``` - -### Params - -#### `query` - -| Param | Type | Description | -| ------- | ------------ | ------------------------------------------------------------- | -| `query` | DocumentNode | A GraphQL query document parsed into an AST by `gql`. | - -#### `options` + - - -### Result - - - -## `useLazyQuery` - -### Example - -```jsx -import { gql, useLazyQuery } from "@apollo/client"; - -const GET_GREETING = gql` - query GetGreeting($language: String!) { - greeting(language: $language) { - message - } - } -`; - -function Hello() { - const [loadGreeting, { called, loading, data }] = useLazyQuery( - GET_GREETING, - { variables: { language: "english" } } - ); - if (called && loading) return

Loading ...

- if (!called) { - return - } - return

Hello {data.greeting.message}!

; -} -``` + +

+[execute: LazyQueryExecFunction<TData, TVariables>, result: QueryResult<TData, TVariables>]
+
-> Refer to the [Queries](../../data/queries/) section for a more in-depth overview of `useLazyQuery`. +A tuple of two values: -### Signature - -```ts -function useLazyQuery( - query: DocumentNode, - options?: LazyQueryHookOptions, -): [ - (options?: LazyQueryHookOptions) => Promise>, - LazyQueryResult -] {} -``` - -### Params - -#### `query` - -| Param | Type | Description | -| ------- | ------------ | ------------------------------------------------------------- | -| `query` | DocumentNode | A GraphQL query document parsed into an AST by `gql`. | - -#### `options` - - - -### Result tuple - -**Execute function (first tuple item)** - -| Param | Type | Description | -| ---------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | -| Execute function | `(options?: LazyQueryHookOptions) => Promise>` | Function that can be triggered to execute the suspended query. After being called, `useLazyQuery` behaves just like `useQuery`. The `useLazyQuery` function returns a promise that fulfills with a query result when the query succeeds or fails. | - -**`LazyQueryResult` object (second tuple item)** - - - -## `useMutation` - -### Example - -```jsx -import { gql, useMutation } from '@apollo/client'; - -const ADD_TODO = gql` - mutation AddTodo($type: String!) { - addTodo(type: $type) { - id - type - } +) => Promise>", + description: "Function that can be triggered to execute the suspended query. After being called, `useLazyQuery` behaves just like `useQuery`. The `useLazyQuery` function returns a promise that fulfills with a query result when the query succeeds or fails." + }, + { + name: "result", + type: "QueryResult", + description: "The result of the query. See the `useQuery` hook for more details.", + canonicalReference: "@apollo/client!QueryResult:interface" } -`; - -function AddTodo() { - let input; - const [addTodo, { data }] = useMutation(ADD_TODO); - - return ( +]}/> +} +/> + + -
{ - e.preventDefault(); - addTodo({ variables: { type: input.value } }); - input.value = ''; - }} - > - { - input = node; - }} - /> - -
+
+        
+        {`[
+  mutate: (options?: MutationFunctionOptions) => Promise>,
+  result: MutationResult
+]`}
+        
+      
+ A tuple of two values: + + ) => Promise>`, + description:
+A function to trigger the mutation from your UI. You can optionally pass this function any of the following options: + +
    +
  • awaitRefetchQueries
  • +
  • context
  • +
  • fetchPolicy
  • +
  • onCompleted
  • +
  • onError
  • +
  • optimisticResponse
  • +
  • refetchQueries
  • +
  • onQueryUpdated
  • +
  • update
  • +
  • variables
  • +
  • client
  • +
+ +Any option you pass here overrides any existing value for that option that you passed to useMutation. + +The mutate function returns a promise that fulfills with your mutation result. +
, + }, + { + name: "result", + type: "MutationResult", + description: "The result of the mutation.", + canonicalReference: "@apollo/client!MutationResult:interface", + }, + ]} + /> - ); -} -``` - -> Refer to the [Mutations](../../data/mutations/) section for a more in-depth overview of `useMutation`. - -### Signature - -```ts -function useMutation( - mutation: DocumentNode, - options?: MutationHookOptions, -): MutationTuple {} -``` - -### Params - -#### `mutation` - -| Param | Type | Description | -| ---------- | ------------ | ---------------------------------------------------------------- | -| `mutation` | DocumentNode | A GraphQL mutation document parsed into an AST by `gql`. | - -#### `options` - - - -### `MutationTuple` result tuple - - - -## `useSubscription` - -### Example - -```jsx -const COMMENTS_SUBSCRIPTION = gql` - subscription OnCommentAdded($repoFullName: String!) { - commentAdded(repoFullName: $repoFullName) { - id - content - } } -`; - -function DontReadTheComments({ repoFullName }) { - const { - data: { commentAdded }, - loading, - } = useSubscription(COMMENTS_SUBSCRIPTION, { variables: { repoFullName } }); - return

New comment: {!loading && commentAdded.content}

; -} -``` - -> Refer to the [Subscriptions](../../data/subscriptions/) section for a more in-depth overview of `useSubscription`. - -#### Subscriptions and React 18 Automatic Batching - -With React 18's [automatic batching](https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching), multiple state updates may be grouped into a single re-render for better performance. - -If your subscription API sends multiple messages at the same time or in very fast succession (within fractions of a millisecond), it is likely that only the last message received in that narrow time frame will result in a re-render. +/> -Consider the following component: + -```jsx -export function Subscriptions() { - const { data, error, loading } = useSubscription(query); - const [accumulatedData, setAccumulatedData] = useState([]); - - useEffect(() => { - setAccumulatedData((prev) => [...prev, data]); - }, [data]); - - return ( - <> - {loading &&

Loading...

} - {JSON.stringify(accumulatedData, undefined, 2)} - - ); -} -``` - -If your subscription back-end emits two messages with the same timestamp, only the last message received by Apollo Client will be rendered. This is because React 18 will batch these two state updates into a single re-render. - -Since the component above is using `useEffect` to push `data` into a piece of local state on each `Subscriptions` re-render, the first message will never be added to the `accumulatedData` array since its render was skipped. + -Instead of using `useEffect` here, we can re-write this component to use the `onData` callback function accepted in `useSubscription`'s `options` object: - -```jsx -export function Subscriptions() { - const [accumulatedData, setAccumulatedData] = useState([]); - const { data, error, loading } = useSubscription( - query, - { - onData({ data }) { - setAccumulatedData((prev) => [...prev, data]) - } - } - ); - - return ( - <> - {loading &&

Loading...

} - {JSON.stringify(accumulatedData, undefined, 2)} - - ); -} -``` - -> ⚠️ **Note:** The `useSubscription` option `onData` is available in Apollo Client >= 3.7. In previous versions, the equivalent option is named `onSubscriptionData`. - -Now, the first message will be added to the `accumulatedData` array since `onData` is called _before_ the component re-renders. React 18 automatic batching is still in effect and results in a single re-render, but with `onData` we can guarantee each message received after the component mounts is added to `accumulatedData`. - -### Signature - -```ts -function useSubscription( - subscription: DocumentNode, - options?: SubscriptionHookOptions, -): { - variables: TVariables; - loading: boolean; - data?: TData; - error?: ApolloError; -} {} -``` - -### Params - -#### `subscription` - -| Param | Type | Description | -| -------------- | ------------ | -------------------------------------------------------------------- | -| `subscription` | DocumentNode | A GraphQL subscription document parsed into an AST by `gql`. | - -#### `options` - - - -### Result - - - -## `useApolloClient` - -### Example - -```jsx -import { useApolloClient } from '@apollo/client'; - -function SomeComponent() { - const client = useApolloClient(); - // `client` is now set to the `ApolloClient` instance being used by the - // application (that was configured using something like `ApolloProvider`) -} -``` - -### Signature - -```ts -function useApolloClient(): ApolloClient {} -``` - -### Result - -| Param | Type | Description | -| ---------------------- | -------------------------- | ---------------------------------------------------------- | -| Apollo Client instance | ApolloClient<object> | The `ApolloClient` instance being used by the application. | - - -## `useReactiveVar` - -Reads the value of a [reactive variable](../../local-state/reactive-variables/) and re-renders the containing component whenever that variable's value changes. This enables a reactive variable to trigger changes _without_ relying on the `useQuery` hook. - -### Example - -```jsx -import { makeVar, useReactiveVar } from "@apollo/client"; -export const cartItemsVar = makeVar([]); - -export function Cart() { - const cartItems = useReactiveVar(cartItemsVar); - // ... -``` - -### Signature - -```tsx -function useReactiveVar(rv: ReactiveVar): T {} -``` + @@ -443,7 +188,6 @@ function useFragment< optimistic?: boolean; variables?: TVars; returnPartialData?: boolean; - canonizeResults?: boolean; }): UseFragmentResult {} ``` @@ -521,7 +265,11 @@ function useSuspenseQuery( Instead of passing a `SuspenseQueryHookOptions` object into the hook, you can also pass a [`skipToken`](#skiptoken) to prevent the `useSuspenseQuery` hook from executing the query or suspending. - + ### Result @@ -660,6 +408,14 @@ function useReadQuery( +} +/> + + + ## `skipToken` diff --git a/docs/source/api/react/preloading.mdx b/docs/source/api/react/preloading.mdx new file mode 100644 index 00000000000..644fe13d122 --- /dev/null +++ b/docs/source/api/react/preloading.mdx @@ -0,0 +1,11 @@ +--- +title: Preloading +description: Apollo Client preloading API reference +minVersion: 3.9.0 +api_doc: + - "@apollo/client!createQueryPreloader:function(1)" +--- + +import { FunctionDetails } from '../../../shared/ApiDoc'; + + diff --git a/docs/source/caching/garbage-collection.mdx b/docs/source/caching/garbage-collection.mdx index b77643a255a..80fc7e768a8 100644 --- a/docs/source/caching/garbage-collection.mdx +++ b/docs/source/caching/garbage-collection.mdx @@ -33,6 +33,10 @@ cache.gc({ }) ``` +> **⚠️ Deprecation warning for `canonizeResults**: +> Using `canonizeResults` can result in memory leaks so we generally do not recommend using this option anymore. +> A future version of Apollo Client will contain a similar feature without the risk of memory leaks. + These additional `cache.gc` options can be useful for investigating memory usage patterns or leaks. Before taking heap snapshots or recording allocation timelines, it's a good idea to force _JavaScript_ garbage collection using your browser's devtools, to ensure memory released by the cache has been fully collected and returned to the heap. ### Configuring garbage collection diff --git a/docs/source/caching/memory-management.mdx b/docs/source/caching/memory-management.mdx new file mode 100644 index 00000000000..cf40fc27d8d --- /dev/null +++ b/docs/source/caching/memory-management.mdx @@ -0,0 +1,126 @@ +--- +title: Memory management +api_doc: + - "@apollo/client!CacheSizes:interface" + - "@apollo/client!ApolloClient:class" +subtitle: Learn how to choose and set custom cache sizes +description: Learn how to choose and set custom cache sizes with Apollo Client. +minVersion: 3.9.0 +--- + +import { Remarks, PropertySignatureTable, Example } from '../../shared/ApiDoc'; + +## Cache Sizes + +For better performance, Apollo Client caches (or, in other words, memoizes) many +internally calculated values. +In most cases, these values are cached in [weak caches](https://en.wikipedia.org/wiki/Weak_reference), which means that if the +source object is garbage-collected, the cached value will be garbage-collected, +too. + +These caches are also Least Recently Used (LRU) caches, meaning that if the cache is full, +the least recently used value will be garbage-collected. + +Depending on your application, you might want to tweak the cache size to fit your +needs. + +You can set your cache size [before (recommended)](#setting-cache-sizes-before-loading-the-apollo-client-library) or [after](#adjusting-cache-sizes-after-loading-the-apollo-client-library) loading the Apollo Client library. + +### Setting cache sizes before loading the Apollo Client library + +Setting cache sizes before loading the Apollo Client library is recommended because some caches are already initialized when the library is loaded. Changed cache sizes only +affect caches created after the fact, so you'd have to write additional runtime code to recreate these caches after changing their size. + + ```ts +import type { CacheSizes } from '@apollo/client/utilities'; + + globalThis[Symbol.for("apollo.cacheSize")] = { + parser: 100, + "fragmentRegistry.lookup": 500 + } satisfies Partial + ``` + +### Adjusting cache sizes after loading the Apollo Client library + +You can also adjust cache sizes after loading the library. + +```js +import { cacheSizes } from '@apollo/client/utilities'; +import { print } from '@apollo/client' + +cacheSizes.print = 100; +// cache sizes changed this way will only take effect for caches +// created after the cache size has been changed, so we need to +// reset the cache for it to be effective + +print.reset(); +``` + +### Choosing appropriate cache sizes + + + +To choose good sizes for our memoization caches, you need to know what they +use as source values, and have a general understanding of the data flow inside of +Apollo Client. + +For most memoized values, the source value is a parsed GraphQL document— +a `DocumentNode`. There are two types: + +* **User-supplied `DocumentNode`s** are created + by the user, for example by using the `gql` template literal tag. + This is the `QUERY`, `MUTATION`, or `SUBSCRIPTION` argument passed + into a [`useQuery` hook](../data/queries/#usequery-api) or as the `query` option to `client.query`. +* **Transformed `DocumentNode`s** are derived from + user-supplied `DocumentNode`s, for example, by applying [`DocumentTransform`s](../data/document-transforms/) to them. + +As a rule of thumb, you should set the cache sizes for caches using a transformed +`DocumentNode` at least to the same size as for caches using a user-supplied +`DocumentNode`. If your application uses a custom `DocumentTransform` that does +not always transform the same input to the same output, you should set the cache +size for caches using a Transformed `DocumentNode` to a higher value than for +caches using a user-supplied `DocumentNode`. + +By default, Apollo Client uses a base value of 1000 cached objects for caches using +user-supplied `DocumentNode` instances, and scales other cache sizes relative +to that. For example, the default base value of 1000 for user-provided `DocumentNode`s would scale to 2000, 4000, etc. for transformed `DocumentNode`s, depending on the transformation performed. + +This base value should be plenty for most applications, but you can tweak them if you have different requirements. + +#### Measuring cache usage + +Since estimating appropriate cache sizes for your application can be hard, Apollo Client +exposes an API for cache usage measurement.
+This way, you can click around in your application and then take a look at the +actual usage of the memoizing caches. + +Keep in mind that this API is primarily meant for usage with the Apollo DevTools +(an integration is coming soon), and the API may change at any +point in time.
+It is also only included in development builds, not in production builds. + + + +The cache usage API is only meant for manual measurements. Don't rely on it in production code or tests. + + + + + + + +### Cache options + + diff --git a/docs/source/config.json b/docs/source/config.json index 6862f2e7836..98c46b99f90 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -28,6 +28,7 @@ "Reading and writing": "/caching/cache-interaction", "Garbage collection and eviction": "/caching/garbage-collection", "Customizing field behavior": "/caching/cache-field-behavior", + "Memory Management": "/caching/memory-management", "Advanced topics": "/caching/advanced-topics" }, "Pagination": { @@ -79,6 +80,7 @@ }, "React": { "Hooks": "/api/react/hooks", + "Preloading": "/api/react/preloading", "Testing": "/api/react/testing", "SSR": "/api/react/ssr", "Components (deprecated)": "/api/react/components", diff --git a/docs/source/data/document-transforms.mdx b/docs/source/data/document-transforms.mdx index d9acc5f8a54..96510c4e27e 100644 --- a/docs/source/data/document-transforms.mdx +++ b/docs/source/data/document-transforms.mdx @@ -2,9 +2,10 @@ title: Document transforms description: Make custom modifications to your GraphQL documents minVersion: 3.8.0 +api_doc: + - "@apollo/client!~DocumentTransformOptions:interface" --- - -import DocumentTransformOptions from '../../shared/document-transform-options.mdx'; +import { InterfaceDetails } from '../../shared/ApiDoc'; > This article assumes you're familiar with the [anatomy of a GraphQL query](https://www.apollographql.com/blog/graphql/basics/the-anatomy-of-a-graphql-query/) and the concept of an [abstract syntax tree (AST)](https://en.wikipedia.org/wiki/Abstract_syntax_tree). To explore a GraphQL AST, visit [AST Explorer](https://astexplorer.net/). @@ -29,7 +30,7 @@ const client = new ApolloClient({ }); ``` -### Lifecycle +### Lifecycle Apollo Client runs document transforms before every GraphQL request for all operations. This extends to any API that performs a network request, such as the [`useQuery`](/react/api/react/hooks#usequery) hook or the [`refetch`](/react/api/core/ObservableQuery#refetch) function on [`ObservableQuery`](/react/api/core/ObservableQuery). @@ -111,11 +112,11 @@ const transformedDocument = visit(document, { ``` > Returning `undefined` from our `Field` visitor tells the `visit` function to leave the node unchanged. -Now that we've determined we are working with the `currentUser` field, we need to figure out if our `id` field is already part of the `currentUser` field's selection set. This ensures we don't accidentally select the field twice in our query. +Now that we've determined we are working with the `currentUser` field, we need to figure out if our `id` field is already part of the `currentUser` field's selection set. This ensures we don't accidentally select the field twice in our query. To do so, let's get the field's `selectionSet` property and loop over its `selections` property to determine if the `id` field is included. -It's important to note that a `selectionSet` may contain `selections` of both fields and fragments. Our implementation only needs to perform checks against fields, so we also check the selection's `kind` property. If we find a match on a field named `id`, we can stop traversal of the AST. +It's important to note that a `selectionSet` may contain `selections` of both fields and fragments. Our implementation only needs to perform checks against fields, so we also check the selection's `kind` property. If we find a match on a field named `id`, we can stop traversal of the AST. We will bring in both the [`Kind`](https://graphql.org/graphql-js/language/#kind) enum from `graphql-js`, which allows us to compare against the selection's `kind` property, and the [`BREAK`](https://graphql.org/graphql-js/language/#break) sentinel, which directs the `visit` function to stop traversal of the AST. @@ -160,7 +161,7 @@ const transformedDocument = visit(document, { Field(field) { // ... const idField = { - // ... + // ... }; return { @@ -224,7 +225,7 @@ const documentTransform = new DocumentTransform((document) => { ### Check our document transform -We can check our custom document transform by calling the `transformDocument` function and passing a GraphQL query to it. +We can check our custom document transform by calling the `transformDocument` function and passing a GraphQL query to it. ```ts import { print } from 'graphql'; @@ -331,7 +332,7 @@ Here `documentTransform1` is combined with `documentTransform2` into a single do #### A note about performance -Combining multiple transforms is a powerful feature that makes it easy to split up transform logic, which can boost maintainability. Depending on the implementation of your visitor, this can result in the traversal of the GraphQL document AST multiple times. Most of the time, this shouldn't be an issue. We recommend using the [`BREAK`](https://graphql.org/graphql-js/language/#break) sentinel from `graphql-js` to prevent unnecessary traversal. +Combining multiple transforms is a powerful feature that makes it easy to split up transform logic, which can boost maintainability. Depending on the implementation of your visitor, this can result in the traversal of the GraphQL document AST multiple times. Most of the time, this shouldn't be an issue. We recommend using the [`BREAK`](https://graphql.org/graphql-js/language/#break) sentinel from `graphql-js` to prevent unnecessary traversal. Suppose you are sending very large queries that require several traversals and have already optimized your visitors with the `BREAK` sentinel. In that case, it's best to combine the transforms into a single visitor that traverses the AST once. @@ -464,7 +465,7 @@ const documentTransform = new DocumentTransform( getCacheKey: (document) => { // Always run the transform function when `shouldCache` is `false` if (shouldCache(document)) { - return [document] + return [document] } } } @@ -507,7 +508,7 @@ const nonCachedTransform = new DocumentTransform(transform, { cache: false }); -const documentTransform = +const documentTransform = cachedTransform .concat(varyingTransform) .concat(conditionalCachedTransform) @@ -527,7 +528,7 @@ Thankfully, GraphQL Code Generator provides a [document transform](https://the-g ```ts title="codegen.ts" {2,12-18} import type { CodegenConfig } from '@graphql-codegen/cli'; import { documentTransform } from './path/to/your/transform'; - + const config: CodegenConfig = { schema: 'https://localhost:4000/graphql', documents: ['src/**/*.tsx'], @@ -574,7 +575,7 @@ Here is an example that uses a DSL-like directive that depends on a feature flag ```ts const query = gql` query MyQuery { - myCustomField @feature(name: "custom", version: 2) + myCustomField @feature(name: "custom", version: 2) } `; @@ -584,7 +585,7 @@ const documentTransform = new DocumentTransform((document) => { documentTransform.transformDocument(query); // query MyQuery($feature_custom_v2: Boolean!) { -// myCustomField @include(if: $feature_custom_v2) +// myCustomField @include(if: $feature_custom_v2) // } ``` @@ -592,4 +593,4 @@ documentTransform.transformDocument(query); ### Options - + diff --git a/docs/source/data/mutations.mdx b/docs/source/data/mutations.mdx index 3a6eec83618..8a7c4c806cf 100644 --- a/docs/source/data/mutations.mdx +++ b/docs/source/data/mutations.mdx @@ -1,10 +1,12 @@ --- title: Mutations in Apollo Client description: Modify data with the useMutation hook +api_doc: + - "@apollo/client!MutationHookOptions:interface" + - "@apollo/client!MutationResult:interface" --- -import MutationOptions3 from '../../shared/mutation-options.mdx'; -import MutationResult3 from '../../shared/mutation-result.mdx'; +import { PropertySignatureTable } from '../../shared/ApiDoc'; Now that we've [learned how to query data](queries/) from our backend with Apollo Client, the natural next step is to learn how to _modify_ back-end data with **mutations**. @@ -380,7 +382,7 @@ detail with usage examples, see the [API reference](../api/react/hooks/). The `useMutation` hook accepts the following options: - + ### Result @@ -388,7 +390,7 @@ The `useMutation` result is a tuple with a mutate function in the first position You call the mutate function to trigger the mutation from your UI. - + ## Next steps diff --git a/docs/source/data/queries.mdx b/docs/source/data/queries.mdx index 8d943e0c2a4..d827a478af4 100644 --- a/docs/source/data/queries.mdx +++ b/docs/source/data/queries.mdx @@ -1,10 +1,12 @@ --- title: Queries description: Fetch data with the useQuery hook +api_doc: + - "@apollo/client!QueryHookOptions:interface" + - "@apollo/client!QueryResult:interface" --- -import QueryOptions from '../../shared/query-options.mdx'; -import QueryResult from '../../shared/query-result.mdx'; +import { PropertySignatureTable } from '../../shared/ApiDoc'; This article shows how to fetch GraphQL data in React with the `useQuery` hook and attach the result to your UI. You'll also learn how Apollo Client simplifies data management code by tracking error and loading states for you. @@ -504,13 +506,13 @@ Most calls to `useQuery` can omit the majority of these options, but it's useful The `useQuery` hook accepts the following options: - + ### Result After being called, the `useQuery` hook returns a result object with the following properties. This object contains your query result, plus some helpful functions for refetching, dynamic polling, and pagination. - + ## Next steps diff --git a/docs/source/data/subscriptions.mdx b/docs/source/data/subscriptions.mdx index c4c177c9784..59e451c44ee 100644 --- a/docs/source/data/subscriptions.mdx +++ b/docs/source/data/subscriptions.mdx @@ -1,10 +1,12 @@ --- title: Subscriptions description: Get real-time updates from your GraphQL server +api_doc: + - "@apollo/client!SubscriptionHookOptions:interface" + - "@apollo/client!SubscriptionResult:interface" --- -import SubscriptionOptions from '../../shared/subscription-options.mdx'; -import SubscriptionResult from '../../shared/subscription-result.mdx'; +import { PropertySignatureTable } from '../../shared/ApiDoc'; In addition to [queries](./queries/) and [mutations](./mutations/), GraphQL supports a third operation type: **subscriptions**. @@ -49,6 +51,51 @@ To use Apollo Client with a GraphQL endpoint that supports [multipart subscripti Aside from updating your client version, no additional configuration is required! Apollo Client automatically sends the required headers with the request if the terminating `HTTPLink` is passed a subscription operation. +#### Usage with Relay or urql + +To consume a multipart subscription over HTTP in an app using Relay or urql, Apollo Client provides network layer adapters that handle the parsing of the multipart response format. + +##### Relay + +```ts +import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/relay"; +import { Environment, Network, RecordSource, Store } from "relay-runtime"; + +const fetchMultipartSubs = createFetchMultipartSubscription( + "https://api.example.com" +); + +const network = Network.create(fetchQuery, fetchMultipartSubs); + +export const RelayEnvironment = new Environment({ + network, + store: new Store(new RecordSource()), +}); +``` + +#### urql + +```ts +import { createFetchMultipartSubscription } from "@apollo/client/utilities/subscriptions/urql"; +import { Client, fetchExchange, subscriptionExchange } from "@urql/core"; + +const url = "https://api.example.com"; + +const multipartSubscriptionForwarder = createFetchMultipartSubscription( + url +); + +const client = new Client({ + url, + exchanges: [ + fetchExchange, + subscriptionExchange({ + forwardSubscription: multipartSubscriptionForwarder, + }), + ], +}); +``` + ## Defining a subscription You define a subscription on both the server side and the client side, just like you do for queries and mutations. @@ -328,13 +375,13 @@ export function CommentsPage({subscribeToNewComments}) { The `useSubscription` Hook accepts the following options: - + ### Result After being called, the `useSubscription` Hook returns a result object with the following properties: - + ## The older `subscriptions-transport-ws` library diff --git a/docs/source/development-testing/testing.mdx b/docs/source/development-testing/testing.mdx index 9821626749c..d32222cb6ab 100644 --- a/docs/source/development-testing/testing.mdx +++ b/docs/source/development-testing/testing.mdx @@ -101,7 +101,7 @@ Each mock object defines a `request` field (indicating the shape and variables o Alternatively, the `result` field can be a function that returns a mocked response after performing arbitrary logic: ```jsx -result: () => { +result: (variables) => { // `variables` is optional // ...arbitrary logic... return { @@ -150,6 +150,77 @@ it("renders without error", async () => { +#### Reusing mocks + +By default, a mock is only used once. If you want to reuse a mock for multiple operations, you can set the `maxUsageCount` field to a number indicating how many times the mock should be used: + + + +```jsx title="dog.test.js" +import { GET_DOG_QUERY } from "./dog"; + +const mocks = [ + { + request: { + query: GET_DOG_QUERY, + variables: { + name: "Buck" + } + }, + result: { + data: { + dog: { id: "1", name: "Buck", breed: "bulldog" } + } + }, + maxUsageCount: 2, // The mock can be used twice before it's removed, default is 1 + } +]; +``` + + + +Passing `Number.POSITIVE_INFINITY` will cause the mock to be reused indefinitely. + +### Dynamic variables + +Sometimes, the exact value of the variables being passed are not known. The `MockedResponse` object takes a `variableMatcher` property that is a function that takes the variables and returns a boolean indication if this mock should match the invocation for the provided query. You cannot specify this parameter and `request.variables` at the same time. + +For example, this mock will match all dog queries: + +```ts +import { MockedResponse } from "@apollo/client/testing"; + +const dogMock: MockedResponse = { + request: { + query: GET_DOG_QUERY + }, + variableMatcher: (variables) => true, + result: { + data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } }, + }, +}; +``` + +This can also be useful for asserting specific variables individually: + +```ts +import { MockedResponse } from "@apollo/client/testing"; + +const dogMock: MockedResponse = { + request: { + query: GET_DOG_QUERY + }, + variableMatcher: jest.fn().mockReturnValue(true), + result: { + data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } }, + }, +}; + +expect(variableMatcher).toHaveBeenCalledWith(expect.objectContaining({ + name: 'Buck' +})); +``` + ### Setting `addTypename` In the example above, we set the `addTypename` prop of `MockedProvider` to `false`. This prevents Apollo Client from automatically adding the special `__typename` field to every object it queries for (it does this by default to support data normalization in the cache). diff --git a/docs/source/performance/optimistic-ui.mdx b/docs/source/performance/optimistic-ui.mdx index 35332dce9e2..a6b15b48efd 100644 --- a/docs/source/performance/optimistic-ui.mdx +++ b/docs/source/performance/optimistic-ui.mdx @@ -73,6 +73,51 @@ As this example shows, the value of `optimisticResponse` is an object that match 5. Apollo Client notifies all affected queries again. The associated components re-render, but if the server's response matches our `optimisticResponse`, this is invisible to the user. +## Bailing out of an optimistic update + +In some cases you may want to skip an optimistic update. For example, you may want to perform an optimistic update _only_ when certain variables are passed to the mutation. To skip an update, pass a function to the `optimisticResponse` option and return the `IGNORE` sentinel object available on the second argument to bail out of the optimistic update. + +Consider this example: + +```tsx +const UPDATE_COMMENT = gql` + mutation UpdateComment($commentId: ID!, $commentContent: String!) { + updateComment(commentId: $commentId, content: $commentContent) { + id + __typename + content + } + } +`; + +function CommentPageWithData() { + const [mutate] = useMutation(UPDATE_COMMENT); + + return ( + + mutate({ + variables: { commentId, commentContent }, + optimisticResponse: (vars, { IGNORE }) => { + if (commentContent === "foo") { + // conditionally bail out of optimistic updates + return IGNORE; + } + return { + updateComment: { + id: commentId, + __typename: "Comment", + content: commentContent + } + } + }, + }) + } + /> + ); +} +``` + ## Example: Adding a new object to a list The previous example shows how to provide an optimistic result for an object that's _already_ in the Apollo Client cache. But what about a mutation that creates a _new_ object? This works similarly. diff --git a/integration-tests/peerdeps-tsc/.gitignore b/integration-tests/peerdeps-tsc/.gitignore new file mode 100644 index 00000000000..db6846cb27a --- /dev/null +++ b/integration-tests/peerdeps-tsc/.gitignore @@ -0,0 +1,5 @@ +# explicitly avoiding to check in this one +# so we run this test always with the latest version +package-lock.json +dist +node_modules diff --git a/integration-tests/peerdeps-tsc/package.json b/integration-tests/peerdeps-tsc/package.json new file mode 100644 index 00000000000..15fa22cb969 --- /dev/null +++ b/integration-tests/peerdeps-tsc/package.json @@ -0,0 +1,20 @@ +{ + "name": "peerdeps-tsc", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "tsc" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "graphql": "^16.0.0", + "graphql-ws": "^5.5.5", + "@types/react": "^18.0.0", + "@types/react-dom": "^18.0.0", + "subscriptions-transport-ws": "^0.11.0", + "typescript": "latest" + } +} diff --git a/integration-tests/peerdeps-tsc/src/index.ts b/integration-tests/peerdeps-tsc/src/index.ts new file mode 100644 index 00000000000..a0266127931 --- /dev/null +++ b/integration-tests/peerdeps-tsc/src/index.ts @@ -0,0 +1 @@ +export * from "@apollo/client"; diff --git a/integration-tests/peerdeps-tsc/tsconfig.json b/integration-tests/peerdeps-tsc/tsconfig.json new file mode 100644 index 00000000000..1dff851d2ac --- /dev/null +++ b/integration-tests/peerdeps-tsc/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "es2016", + "module": "commonjs", + "rootDir": "./src", + "outDir": "./dist", + "declaration": true, + "sourceMap": true, + "declarationMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": false, + "types": ["react", "react-dom"], + "lib": ["es2018", "dom"] + } +} diff --git a/netlify.toml b/netlify.toml index 67879a8b0ba..2a741ec142c 100644 --- a/netlify.toml +++ b/netlify.toml @@ -13,7 +13,7 @@ npm run docmodel cd ../ rm -rf monodocs - git clone https://github.com/apollographql/docs --branch main --single-branch monodocs + git clone https://github.com/apollographql/docs --branch pr/apidoc-enums-since --single-branch monodocs cd monodocs npm i cp -r ../docs local diff --git a/package-lock.json b/package-lock.json index 3df14da6ad9..e77230ea76b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,14 @@ "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", "@wry/equality": "^0.5.6", "@wry/trie": "^0.5.0", "graphql-tag": "^2.12.6", "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", + "rehackt": "0.0.3", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", @@ -48,6 +50,7 @@ "@types/node-fetch": "2.6.11", "@types/react": "18.2.48", "@types/react-dom": "18.2.18", + "@types/relay-runtime": "14.1.14", "@types/use-sync-external-store": "0.0.6", "@typescript-eslint/eslint-plugin": "6.19.1", "@typescript-eslint/parser": "6.19.1", @@ -104,7 +107,7 @@ "npm": "^7.20.3 || ^8.0.0 || ^9.0.0 || ^10.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", + "graphql": "^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", @@ -1763,9 +1766,9 @@ "dev": true }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.20.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.20.0.tgz", - "integrity": "sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==", + "version": "13.23.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", + "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -3055,18 +3058,18 @@ } }, "node_modules/@testing-library/react/node_modules/@testing-library/dom": { - "version": "9.3.3", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.3.tgz", - "integrity": "sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.0.0.tgz", + "integrity": "sha512-+/TLgKNFsYUshOY/zXsQOk+PlFQK+eyJ9T13IDVNJEi+M+Un7xlJK+FZKkbGSnf0+7E1G6PlDhkSYQ/GFiruBQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", + "aria-query": "^5.0.0", "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", + "lz-string": "^1.4.4", "pretty-format": "^27.0.2" }, "engines": { @@ -3074,9 +3077,9 @@ } }, "node_modules/@testing-library/react/node_modules/@types/aria-query": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", - "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.1.tgz", + "integrity": "sha512-XTIieEY+gvJ39ChLcB4If5zHtPxt3Syj5rgZR+e1ctpmK8NjPf0zFqsz4JpLJT0xla9GFDKjy8Cpu331nrmE1Q==", "dev": true }, "node_modules/@testing-library/user-event": { @@ -3434,6 +3437,12 @@ "@types/react": "*" } }, + "node_modules/@types/relay-runtime": { + "version": "14.1.14", + "resolved": "https://registry.npmjs.org/@types/relay-runtime/-/relay-runtime-14.1.14.tgz", + "integrity": "sha512-uG5GJhlyhqBp4j5b4xeH9LFMAr+xAFbWf1Q4ZLa0aLFJJNbjDVmHbzqzuXb+WqNpM3V7LaKwPB1m7w3NYSlCMg==", + "dev": true + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz", @@ -3793,9 +3802,9 @@ "dev": true }, "node_modules/@wry/caches": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.1.tgz", - "integrity": "sha512-bXuaUNLVVkD20wcGBWRyo7j9N3TxePEWFZj2Y+r9OoUzfqmavM84+mFykRicNsBqatba5JLay1t48wxaXaWnlA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@wry/caches/-/caches-1.0.0.tgz", + "integrity": "sha512-FHRUDe2tqrXAj6A/1D39No68lFWbbnh+NCpG9J/6idhL/2Mb/AaxBTYg/sbUVImEo8a4mWeOewUlB1W7uLjByA==", "dependencies": { "tslib": "^2.3.0" }, @@ -4014,25 +4023,12 @@ } }, "node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "dependencies": { - "deep-equal": "^2.0.5" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz", + "integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6.0" } }, "node_modules/array-includes": { @@ -4442,9 +4438,9 @@ } }, "node_modules/builtins/node_modules/semver": { - "version": "7.5.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", - "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -5073,38 +5069,6 @@ } } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5187,12 +5151,11 @@ } }, "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", "dev": true, "dependencies": { - "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" }, @@ -5437,26 +5400,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-shim-unscopables": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.0.tgz", @@ -7171,13 +7114,13 @@ "dev": true }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", "side-channel": "^1.0.4" }, "engines": { @@ -7200,20 +7143,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -7401,15 +7330,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", @@ -7508,15 +7428,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", @@ -7608,15 +7519,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-weakref": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", @@ -7629,19 +7531,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -9170,9 +9059,9 @@ "dev": true }, "node_modules/lz-string": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", - "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz", + "integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=", "dev": true, "bin": { "lz-string": "bin/bin.js" @@ -9304,9 +9193,9 @@ } }, "node_modules/marked-terminal/node_modules/type-fest": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.12.0.tgz", - "integrity": "sha512-qj9wWsnFvVEMUDbESiilKeXeHL7FwwiFcogfhfyjmvT968RXSvnl23f1JOClTHYItsi7o501C/7qVllscUP3oA==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", "dev": true, "engines": { "node": ">=14.16" @@ -10054,9 +9943,9 @@ } }, "node_modules/patch-package/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true, "engines": { "node": ">= 10.0.0" @@ -10656,14 +10545,14 @@ "dev": true }, "node_modules/regexp.prototype.flags": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "set-function-name": "^2.0.0" + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -10672,6 +10561,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/rehackt": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.0.3.tgz", + "integrity": "sha512-aBRHudKhOWwsTvCbSoinzq+Lej/7R8e8UoPvLZo5HirZIIBLGAgdG7SL9QpdcBoQ7+3QYPi3lRLknAzXBlhZ7g==", + "peerDependencies": { + "@types/react": "*", + "react": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -11036,20 +10942,6 @@ "node": ">= 0.4" } }, - "node_modules/set-function-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", - "dev": true, - "dependencies": { - "define-data-property": "^1.0.1", - "functions-have-names": "^1.2.3", - "has-property-descriptors": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -11477,18 +11369,6 @@ "node": ">=8" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", - "dev": true, - "dependencies": { - "internal-slot": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/stream-transform": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-2.1.3.tgz", @@ -12560,21 +12440,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/which-collection": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", - "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", - "dev": true, - "dependencies": { - "is-map": "^2.0.1", - "is-set": "^2.0.1", - "is-weakmap": "^2.0.1", - "is-weakset": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", @@ -12595,16 +12460,17 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.13.tgz", - "integrity": "sha512-P5Nra0qjSncduVPEAr7xhoF5guty49ArDTwzJ/yNuPIbZppyRxFQsRCWrocxIY+CnMVG+qfbU2FmDKyvSGClow==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.4", + "call-bind": "^1.0.2", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.10" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index 144590fcee7..458bd6d5b28 100644 --- a/package.json +++ b/package.json @@ -42,14 +42,14 @@ "prepdist": "node ./config/prepareDist.js", "prepdist:changesets": "ts-node-script config/prepareChangesetsRelease.ts", "postprocess-dist": "ts-node-script config/postprocessDist.ts", - "extract-api": "ts-node-script config/apiExtractor.ts --generate apiReport", + "extract-api": "npm run build && ts-node-script config/apiExtractor.ts --generate apiReport", "clean": "rimraf dist coverage lib temp", "check:format": "prettier --check .", "ci:precheck": "node config/precheck.js", "format": "prettier --write .", "lint": "eslint 'src/**/*.{[jt]s,[jt]sx}'", "inline-inherit-doc": "ts-node-script config/inlineInheritDoc.ts", - "test": "jest --config ./config/jest.config.js", + "test": "node --expose-gc ./node_modules/jest/bin/jest.js --config ./config/jest.config.js", "test:debug": "node --inspect-brk node_modules/.bin/jest --config ./config/jest.config.js --runInBand --testTimeout 99999 --logHeapUsage", "test:ci": "TEST_ENV=ci npm run test:coverage -- --logHeapUsage && npm run test:memory", "test:watch": "jest --config ./config/jest.config.js --watch", @@ -70,7 +70,7 @@ "npm": "^7.20.3 || ^8.0.0 || ^9.0.0 || ^10.0.0" }, "peerDependencies": { - "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0", + "graphql": "^15.0.0 || ^16.0.0", "graphql-ws": "^5.5.5", "react": "^16.8.0 || ^17.0.0 || ^18.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0", @@ -92,12 +92,14 @@ }, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", + "@wry/caches": "^1.0.0", "@wry/equality": "^0.5.6", "@wry/trie": "^0.5.0", "graphql-tag": "^2.12.6", "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", + "rehackt": "0.0.3", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", @@ -129,6 +131,7 @@ "@types/node-fetch": "2.6.11", "@types/react": "18.2.48", "@types/react-dom": "18.2.18", + "@types/relay-runtime": "14.1.14", "@types/use-sync-external-store": "0.0.6", "@typescript-eslint/eslint-plugin": "6.19.1", "@typescript-eslint/parser": "6.19.1", diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index 6c06e405fd9..9a16f9572d9 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", @@ -59,8 +60,10 @@ Array [ "useBackgroundQuery", "useFragment", "useLazyQuery", + "useLoadableQuery", "useMutation", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", @@ -264,6 +267,7 @@ Array [ "ApolloConsumer", "ApolloProvider", "DocumentType", + "createQueryPreloader", "getApolloContext", "operationName", "parser", @@ -273,8 +277,10 @@ Array [ "useBackgroundQuery", "useFragment", "useLazyQuery", + "useLoadableQuery", "useMutation", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", @@ -316,8 +322,10 @@ Array [ "useBackgroundQuery", "useFragment", "useLazyQuery", + "useLoadableQuery", "useMutation", "useQuery", + "useQueryRefHandlers", "useReactiveVar", "useReadQuery", "useSubscription", @@ -325,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", @@ -380,6 +399,8 @@ Array [ exports[`exports of public entry points @apollo/client/utilities 1`] = ` Array [ + "AutoCleanedStrongCache", + "AutoCleanedWeakCache", "Concast", "DEV", "DeepMerger", @@ -389,12 +410,14 @@ Array [ "argumentsObjectFromField", "asyncMap", "buildQueryFromSelectionSet", + "cacheSizes", "canUseAsyncIteratorSymbol", "canUseDOM", "canUseLayoutEffect", "canUseSymbol", "canUseWeakMap", "canUseWeakSet", + "canonicalStringify", "checkDocument", "cloneDeep", "compact", @@ -477,3 +500,9 @@ Array [ "newInvariantError", ] `; + +exports[`exports of public entry points @apollo/client/utilities/subscriptions/urql 1`] = ` +Array [ + "createFetchMultipartSubscription", +] +`; diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index 2fd8c29f187..c435220e116 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -2920,7 +2920,10 @@ describe("client", () => { return client .query({ query }) .then(({ data }) => { - expect(data).toEqual(result.data); + const { price, ...todoWithoutPrice } = data.todos[0]; + expect(data).toEqual({ + todos: [todoWithoutPrice], + }); }) .then(resolve, reject); }); diff --git a/src/__tests__/exports.ts b/src/__tests__/exports.ts index 819ec6d2c81..181a1cc2b0a 100644 --- a/src/__tests__/exports.ts +++ b/src/__tests__/exports.ts @@ -26,12 +26,14 @@ import * as reactComponents from "../react/components"; import * as reactContext from "../react/context"; import * as reactHOC from "../react/hoc"; import * as reactHooks from "../react/hooks"; +import * as reactInternal from "../react/internal"; import * as reactParser from "../react/parser"; import * as reactSSR from "../react/ssr"; import * as testing from "../testing"; import * as testingCore from "../testing/core"; import * as utilities from "../utilities"; import * as utilitiesGlobals from "../utilities/globals"; +import * as urqlUtilities from "../utilities/subscriptions/urql"; const entryPoints = require("../../config/entryPoints.js"); @@ -70,17 +72,24 @@ describe("exports of public entry points", () => { check("@apollo/client/react/context", reactContext); check("@apollo/client/react/hoc", reactHOC); check("@apollo/client/react/hooks", reactHooks); + check("@apollo/client/react/internal", reactInternal); check("@apollo/client/react/parser", reactParser); check("@apollo/client/react/ssr", reactSSR); check("@apollo/client/testing", testing); check("@apollo/client/testing/core", testingCore); check("@apollo/client/utilities", utilities); check("@apollo/client/utilities/globals", utilitiesGlobals); + check("@apollo/client/utilities/subscriptions/urql", urqlUtilities); it("completeness", () => { const { join } = require("path").posix; entryPoints.forEach((info: Record) => { const id = join("@apollo/client", ...info.dirs); + // We don't want to add a devDependency for relay-runtime, + // and our API extractor job is already validating its public exports, + // so we'll skip the utilities/subscriptions/relay entrypoing here + // since it errors on the `relay-runtime` import. + if (id === "@apollo/client/utilities/subscriptions/relay") return; expect(testedIds).toContain(id); }); }); diff --git a/src/__tests__/mutationResults.ts b/src/__tests__/mutationResults.ts index d7656a9232d..aa9a6183937 100644 --- a/src/__tests__/mutationResults.ts +++ b/src/__tests__/mutationResults.ts @@ -1186,8 +1186,6 @@ describe("mutation results", () => { subscribeAndCount(reject, watchedQuery, (count, result) => { if (count === 1) { - expect(result.data).toEqual({ echo: "a" }); - } else if (count === 2) { expect(result.data).toEqual({ echo: "b" }); client.mutate({ mutation: resetMutation, @@ -1197,7 +1195,7 @@ describe("mutation results", () => { }, }, }); - } else if (count === 3) { + } else if (count === 2) { expect(result.data).toEqual({ echo: "0" }); resolve(); } diff --git a/src/__tests__/optimistic.ts b/src/__tests__/optimistic.ts index 4184a2f8d6d..3cc984868ed 100644 --- a/src/__tests__/optimistic.ts +++ b/src/__tests__/optimistic.ts @@ -982,6 +982,113 @@ describe("optimistic mutation results", () => { resolve(); } ); + + itAsync( + "will not update optimistically if optimisticResponse returns IGNORE sentinel object", + async (resolve, reject) => { + expect.assertions(5); + + let subscriptionHandle: Subscription; + + const client = await setup(reject, { + request: { query: mutation, variables }, + result: mutationResult, + }); + + // we have to actually subscribe to the query to be able to update it + await new Promise((resolve) => { + const handle = client.watchQuery({ query }); + subscriptionHandle = handle.subscribe({ + next(res: any) { + resolve(res); + }, + }); + }); + + const id = "TodoList5"; + const isTodoList = ( + list: unknown + ): list is { todos: { text: string }[] } => + typeof initialList === "object" && + initialList !== null && + "todos" in initialList && + Array.isArray(initialList.todos); + + const initialList = client.cache.extract(true)[id]; + + if (!isTodoList(initialList)) { + reject(new Error("Expected TodoList")); + return; + } + + expect(initialList.todos.length).toEqual(3); + + const promise = client.mutate({ + mutation, + variables, + optimisticResponse: (vars, { IGNORE }) => { + return IGNORE; + }, + update: (proxy: any, mResult: any) => { + expect(mResult.data.createTodo.id).toBe("99"); + + const fragment = gql` + fragment todoList on TodoList { + todos { + id + text + completed + __typename + } + } + `; + + const data: any = proxy.readFragment({ id, fragment }); + + proxy.writeFragment({ + data: { + ...data, + todos: [mResult.data.createTodo, ...data.todos], + }, + id, + fragment, + }); + }, + }); + + const list = client.cache.extract(true)[id]; + + if (!isTodoList(list)) { + reject(new Error("Expected TodoList")); + return; + } + + expect(list.todos.length).toEqual(3); + + await promise; + + const result = await client.query({ query }); + + subscriptionHandle!.unsubscribe(); + + const newList = result.data.todoList; + + if (!isTodoList(newList)) { + reject(new Error("Expected TodoList")); + return; + } + + // There should be one more todo item than before + expect(newList.todos.length).toEqual(4); + + // Since we used `prepend` it should be at the front + expect(newList.todos[0].text).toBe( + "This one was created with a mutation." + ); + + resolve(); + } + ); }); describe("optimistic updates using `updateQueries`", () => { diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index 44f283a2723..a0fd1778bdc 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -2,9 +2,15 @@ import type { DocumentNode } from "graphql"; import { wrap } from "optimism"; import type { StoreObject, Reference } from "../../utilities/index.js"; -import { getFragmentQueryDocument } from "../../utilities/index.js"; +import { + cacheSizes, + defaultCacheSizes, + getFragmentQueryDocument, +} from "../../utilities/index.js"; import type { DataProxy } from "./types/DataProxy.js"; import type { Cache } from "./types/Cache.js"; +import { WeakCache } from "@wry/caches"; +import { getApolloCacheMemoryInternals } from "../../utilities/caching/getMemoryInternals.js"; export type Transaction = (c: ApolloCache) => void; @@ -137,7 +143,12 @@ export abstract class ApolloCache implements DataProxy { // Make sure we compute the same (===) fragment query document every // time we receive the same fragment in readFragment. - private getFragmentDoc = wrap(getFragmentQueryDocument); + private getFragmentDoc = wrap(getFragmentQueryDocument, { + max: + cacheSizes["cache.fragmentQueryDocuments"] || + defaultCacheSizes["cache.fragmentQueryDocuments"], + cache: WeakCache, + }); public readFragment( options: Cache.ReadFragmentOptions, @@ -209,4 +220,17 @@ export abstract class ApolloCache implements DataProxy { }, }); } + + /** + * @experimental + * @internal + * This is not a stable API - it is used in development builds to expose + * information to the DevTools. + * Use at your own risk! + */ + public getMemoryInternals?: typeof getApolloCacheMemoryInternals; +} + +if (__DEV__) { + ApolloCache.prototype.getMemoryInternals = getApolloCacheMemoryInternals; } diff --git a/src/cache/core/types/Cache.ts b/src/cache/core/types/Cache.ts index 58835e6aca5..0fa70742e15 100644 --- a/src/cache/core/types/Cache.ts +++ b/src/cache/core/types/Cache.ts @@ -14,6 +14,13 @@ export namespace Cache { previousResult?: any; optimistic: boolean; returnPartialData?: boolean; + /** + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature without + * the risk of memory leaks. + */ canonizeResults?: boolean; } diff --git a/src/cache/core/types/DataProxy.ts b/src/cache/core/types/DataProxy.ts index 6dbdf47b75d..4e3e0f9cc73 100644 --- a/src/cache/core/types/DataProxy.ts +++ b/src/cache/core/types/DataProxy.ts @@ -68,11 +68,7 @@ export namespace DataProxy { * readQuery method can be omitted. Defaults to false. */ optimistic?: boolean; - /** - * Whether to canonize cache results before returning them. Canonization - * takes some extra time, but it speeds up future deep equality comparisons. - * Defaults to false. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ canonizeResults?: boolean; } @@ -89,11 +85,7 @@ export namespace DataProxy { * readQuery method can be omitted. Defaults to false. */ optimistic?: boolean; - /** - * Whether to canonize cache results before returning them. Canonization - * takes some extra time, but it speeds up future deep equality comparisons. - * Defaults to false. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ canonizeResults?: boolean; } diff --git a/src/cache/core/types/common.ts b/src/cache/core/types/common.ts index 46c4fa8a155..886ccb63458 100644 --- a/src/cache/core/types/common.ts +++ b/src/cache/core/types/common.ts @@ -87,6 +87,10 @@ declare const _invalidateModifier: unique symbol; export interface InvalidateModifier { [_invalidateModifier]: true; } +declare const _ignoreModifier: unique symbol; +export interface IgnoreModifier { + [_ignoreModifier]: true; +} export type ModifierDetails = { DELETE: DeleteModifier; diff --git a/src/cache/index.ts b/src/cache/index.ts index bed4ad849fd..d57341ff2ac 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -14,7 +14,11 @@ export type { export { MissingFieldError } from "./core/types/common.js"; export type { Reference } from "../utilities/index.js"; -export { isReference, makeReference } from "../utilities/index.js"; +export { + isReference, + makeReference, + canonicalStringify, +} from "../utilities/index.js"; export { EntityStore } from "./inmemory/entityStore.js"; export { @@ -38,8 +42,6 @@ export type { } from "./inmemory/policies.js"; export { Policies } from "./inmemory/policies.js"; -export { canonicalStringify } from "./inmemory/object-canon.js"; - export type { FragmentRegistryAPI } from "./inmemory/fragmentRegistry.js"; export { createFragmentRegistry } from "./inmemory/fragmentRegistry.js"; diff --git a/src/cache/inmemory/__tests__/cache.ts b/src/cache/inmemory/__tests__/cache.ts index edaa63fb4d4..2d426ed7207 100644 --- a/src/cache/inmemory/__tests__/cache.ts +++ b/src/cache/inmemory/__tests__/cache.ts @@ -19,6 +19,7 @@ import { StoreWriter } from "../writeToStore"; import { ObjectCanon } from "../object-canon"; import { TypePolicies } from "../policies"; import { spyOnConsole } from "../../../testing/internal"; +import { defaultCacheSizes } from "../../../utilities"; disableFragmentWarnings(); @@ -2119,15 +2120,17 @@ describe("Cache", () => { }); describe("resultCacheMaxSize", () => { - const defaultMaxSize = Math.pow(2, 16); - it("uses default max size on caches if resultCacheMaxSize is not configured", () => { const cache = new InMemoryCache(); - expect(cache["maybeBroadcastWatch"].options.max).toBe(defaultMaxSize); + expect(cache["maybeBroadcastWatch"].options.max).toBe( + defaultCacheSizes["inMemoryCache.maybeBroadcastWatch"] + ); expect(cache["storeReader"]["executeSelectionSet"].options.max).toBe( - defaultMaxSize + defaultCacheSizes["inMemoryCache.executeSelectionSet"] + ); + expect(cache["getFragmentDoc"].options.max).toBe( + defaultCacheSizes["cache.fragmentQueryDocuments"] ); - expect(cache["getFragmentDoc"].options.max).toBe(defaultMaxSize); }); it("configures max size on caches when resultCacheMaxSize is set", () => { @@ -2137,7 +2140,9 @@ describe("resultCacheMaxSize", () => { expect(cache["storeReader"]["executeSelectionSet"].options.max).toBe( resultCacheMaxSize ); - expect(cache["getFragmentDoc"].options.max).toBe(defaultMaxSize); + expect(cache["getFragmentDoc"].options.max).toBe( + defaultCacheSizes["cache.fragmentQueryDocuments"] + ); }); }); diff --git a/src/cache/inmemory/__tests__/client.ts b/src/cache/inmemory/__tests__/client.ts new file mode 100644 index 00000000000..23fd87f6f73 --- /dev/null +++ b/src/cache/inmemory/__tests__/client.ts @@ -0,0 +1,169 @@ +// This file contains InMemoryCache-specific tests that exercise the +// ApolloClient class. Other test modules in this directory only test +// InMemoryCache and related utilities, without involving ApolloClient. + +import { ApolloClient, WatchQueryFetchPolicy, gql } from "../../../core"; +import { ApolloLink } from "../../../link/core"; +import { Observable } from "../../../utilities"; +import { InMemoryCache } from "../.."; +import { subscribeAndCount } from "../../../testing"; + +describe("InMemoryCache tests exercising ApolloClient", () => { + it.each([ + "cache-first", + "network-only", + "cache-and-network", + "cache-only", + "no-cache", + ])( + "results should be read from cache even when incomplete (fetchPolicy %s)", + (fetchPolicy) => { + const dateFromCache = "2023-09-14T13:03:22.616Z"; + const dateFromNetwork = "2023-09-15T13:03:22.616Z"; + + const cache = new InMemoryCache({ + typePolicies: { + Query: { + fields: { + date: { + read(existing) { + return new Date(existing || dateFromCache); + }, + }, + }, + }, + }, + }); + + const client = new ApolloClient({ + link: new ApolloLink( + (operation) => + new Observable((observer) => { + observer.next({ + data: { + // This raw string should be converted to a Date by the Query.date + // read function passed to InMemoryCache below. + date: dateFromNetwork, + // Make sure we don't accidentally return fields not mentioned in + // the query just because the result is incomplete. + ignored: "irrelevant to the subscribed query", + // Note the Query.missing field is, well, missing. + }, + }); + setTimeout(() => { + observer.complete(); + }, 10); + }) + ), + cache, + }); + + const query = gql` + query { + date + missing + } + `; + + const observable = client.watchQuery({ + query, + fetchPolicy, // Varies with each test iteration + returnPartialData: true, + }); + + return new Promise((resolve, reject) => { + subscribeAndCount(reject, observable, (handleCount, result) => { + let adjustedCount = handleCount; + if ( + fetchPolicy === "network-only" || + fetchPolicy === "no-cache" || + fetchPolicy === "cache-only" + ) { + // The network-only, no-cache, and cache-only fetch policies do not + // deliver a loading:true result initially, so we adjust the + // handleCount to skip that case. + ++adjustedCount; + } + + // The only fetch policy that does not re-read results from the cache is + // the "no-cache" policy. In this test, that means the Query.date field + // will remain as a raw string rather than being converted to a Date by + // the read function. + const expectedDateAfterResult = + fetchPolicy === "cache-only" ? new Date(dateFromCache) + : fetchPolicy === "no-cache" ? dateFromNetwork + : new Date(dateFromNetwork); + + if (adjustedCount === 1) { + expect(result.loading).toBe(true); + expect(result.data).toEqual({ + date: new Date(dateFromCache), + }); + } else if (adjustedCount === 2) { + expect(result.loading).toBe(false); + expect(result.data).toEqual({ + date: expectedDateAfterResult, + // The no-cache fetch policy does return extraneous fields from the + // raw network result that were not requested in the query, since + // the cache is not consulted. + ...(fetchPolicy === "no-cache" ? + { + ignored: "irrelevant to the subscribed query", + } + : null), + }); + + if (fetchPolicy === "no-cache") { + // The "no-cache" fetch policy does not receive updates from the + // cache, so we finish the test early (passing). + setTimeout(() => resolve(), 20); + } else { + cache.writeQuery({ + query: gql` + query { + missing + } + `, + data: { + missing: "not missing anymore", + }, + }); + } + } else if (adjustedCount === 3) { + expect(result.loading).toBe(false); + expect(result.data).toEqual({ + date: expectedDateAfterResult, + missing: "not missing anymore", + }); + + expect(cache.extract()).toEqual({ + ROOT_QUERY: { + __typename: "Query", + // The cache-only fetch policy does not receive updates from the + // network, so it never ends up writing the date field into the + // cache explicitly, though Query.date can still be synthesized by + // the read function. + ...(fetchPolicy === "cache-only" ? null : ( + { + // Make sure this field is stored internally as a raw string. + date: dateFromNetwork, + } + )), + // Written explicitly with cache.writeQuery above. + missing: "not missing anymore", + // The ignored field is never written to the cache, because it is + // not included in the query. + }, + }); + + // Wait 20ms to give the test a chance to fail if there are unexpected + // additional results. + setTimeout(() => resolve(), 20); + } else { + reject(new Error(`Unexpected count ${adjustedCount}`)); + } + }); + }); + } + ); +}); diff --git a/src/cache/inmemory/__tests__/key-extractor.ts b/src/cache/inmemory/__tests__/key-extractor.ts index 3636f6490e6..d525263d010 100644 --- a/src/cache/inmemory/__tests__/key-extractor.ts +++ b/src/cache/inmemory/__tests__/key-extractor.ts @@ -1,5 +1,5 @@ import { KeySpecifier } from "../policies"; -import { canonicalStringify } from "../object-canon"; +import { canonicalStringify } from "../../../utilities"; import { getSpecifierPaths, collectSpecifierPaths, diff --git a/src/cache/inmemory/__tests__/readFromStore.ts b/src/cache/inmemory/__tests__/readFromStore.ts index 972e252215c..2af1138cef8 100644 --- a/src/cache/inmemory/__tests__/readFromStore.ts +++ b/src/cache/inmemory/__tests__/readFromStore.ts @@ -17,14 +17,16 @@ import { isReference, TypedDocumentNode, } from "../../../core"; +import { defaultCacheSizes } from "../../../utilities"; describe("resultCacheMaxSize", () => { const cache = new InMemoryCache(); - const defaultMaxSize = Math.pow(2, 16); it("uses default max size on caches if resultCacheMaxSize is not configured", () => { const reader = new StoreReader({ cache }); - expect(reader["executeSelectionSet"].options.max).toBe(defaultMaxSize); + expect(reader["executeSelectionSet"].options.max).toBe( + defaultCacheSizes["inMemoryCache.executeSelectionSet"] + ); }); it("configures max size on caches when resultCacheMaxSize is set", () => { diff --git a/src/cache/inmemory/entityStore.ts b/src/cache/inmemory/entityStore.ts index 5c617ce67a6..c0582843fdc 100644 --- a/src/cache/inmemory/entityStore.ts +++ b/src/cache/inmemory/entityStore.ts @@ -32,6 +32,7 @@ import type { DeleteModifier, ModifierDetails, } from "../core/types/common.js"; +import type { DocumentNode, FieldNode, SelectionSetNode } from "graphql"; const DELETE: DeleteModifier = Object.create(null); const delModifier: Modifier = () => DELETE; @@ -522,6 +523,27 @@ export abstract class EntityStore implements NormalizedCache { } // Used to compute cache keys specific to this.group. + /** overload for `InMemoryCache.maybeBroadcastWatch` */ + public makeCacheKey( + document: DocumentNode, + callback: Cache.WatchCallback, + details: string + ): object; + /** overload for `StoreReader.executeSelectionSet` */ + public makeCacheKey( + selectionSet: SelectionSetNode, + parent: string /* = ( Reference.__ref ) */ | StoreObject, + varString: string | undefined, + canonizeResults: boolean + ): object; + /** overload for `StoreReader.executeSubSelectedArray` */ + public makeCacheKey( + field: FieldNode, + array: readonly any[], + varString: string | undefined + ): object; + /** @deprecated This is only meant for internal usage, + * in your own code please use a `Trie` instance instead. */ public makeCacheKey(...args: any[]): object; public makeCacheKey() { return this.group.keyMaker.lookupArray(arguments); diff --git a/src/cache/inmemory/fragmentRegistry.ts b/src/cache/inmemory/fragmentRegistry.ts index 0832e6a7934..28463594c9d 100644 --- a/src/cache/inmemory/fragmentRegistry.ts +++ b/src/cache/inmemory/fragmentRegistry.ts @@ -6,16 +6,21 @@ import type { } from "graphql"; import { visit } from "graphql"; -import type { OptimisticWrapperFunction } from "optimism"; import { wrap } from "optimism"; import type { FragmentMap } from "../../utilities/index.js"; -import { getFragmentDefinitions } from "../../utilities/index.js"; +import { + cacheSizes, + defaultCacheSizes, + getFragmentDefinitions, +} from "../../utilities/index.js"; +import { WeakCache } from "@wry/caches"; export interface FragmentRegistryAPI { register(...fragments: DocumentNode[]): this; lookup(fragmentName: string): FragmentDefinitionNode | null; transform(document: D): D; + resetCaches(): void; } // As long as createFragmentRegistry is not imported or used, the @@ -65,17 +70,32 @@ class FragmentRegistry implements FragmentRegistryAPI { private invalidate(name: string) {} public resetCaches() { - this.invalidate = (this.lookup = this.cacheUnaryMethod(this.lookup)).dirty; // This dirty function is bound to the wrapped lookup method. - this.transform = this.cacheUnaryMethod(this.transform); - this.findFragmentSpreads = this.cacheUnaryMethod(this.findFragmentSpreads); - } - - private cacheUnaryMethod any>(originalMethod: F) { - return wrap, ReturnType>(originalMethod.bind(this), { + const proto = FragmentRegistry.prototype; + this.invalidate = (this.lookup = wrap(proto.lookup.bind(this), { makeCacheKey: (arg) => arg, - }) as OptimisticWrapperFunction, ReturnType> & F; + max: + cacheSizes["fragmentRegistry.lookup"] || + defaultCacheSizes["fragmentRegistry.lookup"], + })).dirty; // This dirty function is bound to the wrapped lookup method. + this.transform = wrap(proto.transform.bind(this), { + cache: WeakCache, + max: + cacheSizes["fragmentRegistry.transform"] || + defaultCacheSizes["fragmentRegistry.transform"], + }); + this.findFragmentSpreads = wrap(proto.findFragmentSpreads.bind(this), { + cache: WeakCache, + max: + cacheSizes["fragmentRegistry.findFragmentSpreads"] || + defaultCacheSizes["fragmentRegistry.findFragmentSpreads"], + }); } + /* + * Note: + * This method is only memoized so it can serve as a dependency to `tranform`, + * so calling `invalidate` will invalidate cache entries for `transform`. + */ public lookup(fragmentName: string): FragmentDefinitionNode | null { return this.registry[fragmentName] || null; } diff --git a/src/cache/inmemory/inMemoryCache.ts b/src/cache/inmemory/inMemoryCache.ts index 45820e9d258..fe62023f165 100644 --- a/src/cache/inmemory/inMemoryCache.ts +++ b/src/cache/inmemory/inMemoryCache.ts @@ -16,6 +16,10 @@ import { addTypenameToDocument, isReference, DocumentTransform, + canonicalStringify, + print, + cacheSizes, + defaultCacheSizes, } from "../../utilities/index.js"; import type { InMemoryCacheConfig, NormalizedCacheObject } from "./types.js"; import { StoreReader } from "./readFromStore.js"; @@ -24,8 +28,8 @@ import { EntityStore, supportsResultCaching } from "./entityStore.js"; import { makeVar, forgetCache, recallCache } from "./reactiveVars.js"; import { Policies } from "./policies.js"; import { hasOwn, normalizeConfig, shouldCanonizeResults } from "./helpers.js"; -import { canonicalStringify } from "./object-canon.js"; import type { OperationVariables } from "../../core/index.js"; +import { getInMemoryCacheMemoryInternals } from "../../utilities/caching/getMemoryInternals.js"; type BroadcastOptions = Pick< Cache.BatchOptions, @@ -123,7 +127,10 @@ export class InMemoryCache extends ApolloCache { return this.broadcastWatch(c, options); }, { - max: this.config.resultCacheMaxSize, + max: + this.config.resultCacheMaxSize || + cacheSizes["inMemoryCache.maybeBroadcastWatch"] || + defaultCacheSizes["inMemoryCache.maybeBroadcastWatch"], makeCacheKey: (c: Cache.WatchOptions) => { // Return a cache key (thus enabling result caching) only if we're // currently using a data store that can track cache dependencies. @@ -297,6 +304,9 @@ export class InMemoryCache extends ApolloCache { resetResultIdentities?: boolean; }) { canonicalStringify.reset(); + print.reset(); + this.addTypenameTransform.resetCache(); + this.config.fragments?.resetCaches(); const ids = this.optimisticData.gc(); if (options && !this.txCount) { if (options.resetResultCache) { @@ -572,4 +582,17 @@ export class InMemoryCache extends ApolloCache { c.callback((c.lastDiff = diff), lastDiff); } } + + /** + * @experimental + * @internal + * This is not a stable API - it is used in development builds to expose + * information to the DevTools. + * Use at your own risk! + */ + public getMemoryInternals?: typeof getInMemoryCacheMemoryInternals; +} + +if (__DEV__) { + InMemoryCache.prototype.getMemoryInternals = getInMemoryCacheMemoryInternals; } diff --git a/src/cache/inmemory/object-canon.ts b/src/cache/inmemory/object-canon.ts index 36b196d1519..e69a8b1bbeb 100644 --- a/src/cache/inmemory/object-canon.ts +++ b/src/cache/inmemory/object-canon.ts @@ -195,35 +195,3 @@ type SortedKeysInfo = { sorted: string[]; json: string; }; - -// Since the keys of canonical objects are always created in lexicographically -// sorted order, we can use the ObjectCanon to implement a fast and stable -// version of JSON.stringify, which automatically sorts object keys. -export const canonicalStringify = Object.assign( - function (value: any): string { - if (isObjectOrArray(value)) { - if (stringifyCanon === void 0) { - resetCanonicalStringify(); - } - const canonical = stringifyCanon.admit(value); - let json = stringifyCache.get(canonical); - if (json === void 0) { - stringifyCache.set(canonical, (json = JSON.stringify(canonical))); - } - return json; - } - return JSON.stringify(value); - }, - { - reset: resetCanonicalStringify, - } -); - -// Can be reset by calling canonicalStringify.reset(). -let stringifyCanon: ObjectCanon; -let stringifyCache: WeakMap; - -function resetCanonicalStringify() { - stringifyCanon = new ObjectCanon(); - stringifyCache = new (canUseWeakMap ? WeakMap : Map)(); -} diff --git a/src/cache/inmemory/policies.ts b/src/cache/inmemory/policies.ts index 41db9853bb8..3c68f35eb9d 100644 --- a/src/cache/inmemory/policies.ts +++ b/src/cache/inmemory/policies.ts @@ -48,17 +48,11 @@ import type { } from "../core/types/common.js"; import type { WriteContext } from "./writeToStore.js"; -// Upgrade to a faster version of the default stable JSON.stringify function -// used by getStoreKeyName. This function is used when computing storeFieldName -// strings (when no keyArgs has been configured for a field). -import { canonicalStringify } from "./object-canon.js"; import { keyArgsFnFromSpecifier, keyFieldsFnFromSpecifier, } from "./key-extractor.js"; -getStoreKeyName.setStringify(canonicalStringify); - export type TypePolicies = { [__typename: string]: TypePolicy; }; diff --git a/src/cache/inmemory/readFromStore.ts b/src/cache/inmemory/readFromStore.ts index 9ad5455b457..cc1ec9f0f6b 100644 --- a/src/cache/inmemory/readFromStore.ts +++ b/src/cache/inmemory/readFromStore.ts @@ -28,6 +28,9 @@ import { isNonNullObject, canUseWeakMap, compact, + canonicalStringify, + cacheSizes, + defaultCacheSizes, } from "../../utilities/index.js"; import type { Cache } from "../core/types/Cache.js"; import type { @@ -50,7 +53,7 @@ import type { Policies } from "./policies.js"; import type { InMemoryCache } from "./inMemoryCache.js"; import type { MissingTree } from "../core/types/common.js"; import { MissingFieldError } from "../core/types/common.js"; -import { canonicalStringify, ObjectCanon } from "./object-canon.js"; +import { ObjectCanon } from "./object-canon.js"; export type VariableMap = { [name: string]: any }; @@ -152,6 +155,10 @@ export class StoreReader { this.canon = config.canon || new ObjectCanon(); + // memoized functions in this class will be "garbage-collected" + // by recreating the whole `StoreReader` in + // `InMemoryCache.resetResultsCache` + // (triggered from `InMemoryCache.gc` with `resetResultCache: true`) this.executeSelectionSet = wrap( (options) => { const { canonizeResults } = options.context; @@ -188,7 +195,10 @@ export class StoreReader { return this.execSelectionSetImpl(options); }, { - max: this.config.resultCacheMaxSize, + max: + this.config.resultCacheMaxSize || + cacheSizes["inMemoryCache.executeSelectionSet"] || + defaultCacheSizes["inMemoryCache.executeSelectionSet"], keyArgs: execSelectionSetKeyArgs, // Note that the parameters of makeCacheKey are determined by the // array returned by keyArgs. @@ -214,7 +224,10 @@ export class StoreReader { return this.execSubSelectedArrayImpl(options); }, { - max: this.config.resultCacheMaxSize, + max: + this.config.resultCacheMaxSize || + cacheSizes["inMemoryCache.executeSubSelectedArray"] || + defaultCacheSizes["inMemoryCache.executeSubSelectedArray"], makeCacheKey({ field, array, context }) { if (supportsResultCaching(context.store)) { return context.store.makeCacheKey(field, array, context.varString); diff --git a/src/cache/inmemory/types.ts b/src/cache/inmemory/types.ts index 5a8d66ec300..10678ce5ad7 100644 --- a/src/cache/inmemory/types.ts +++ b/src/cache/inmemory/types.ts @@ -119,6 +119,13 @@ export type ReadQueryOptions = { query: DocumentNode; variables?: Object; previousResult?: any; + /** + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature without + * the risk of memory leaks. + */ canonizeResults?: boolean; rootId?: string; config?: ApolloReducerConfig; @@ -137,7 +144,17 @@ export interface InMemoryCacheConfig extends ApolloReducerConfig { resultCaching?: boolean; possibleTypes?: PossibleTypesMap; typePolicies?: TypePolicies; + /** + * @deprecated + * Please use `cacheSizes` instead. + */ resultCacheMaxSize?: number; + /** + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature. + */ canonizeResults?: boolean; fragments?: FragmentRegistryAPI; } diff --git a/src/cache/inmemory/writeToStore.ts b/src/cache/inmemory/writeToStore.ts index fd388a0180c..a88c875e268 100644 --- a/src/cache/inmemory/writeToStore.ts +++ b/src/cache/inmemory/writeToStore.ts @@ -25,6 +25,7 @@ import { addTypenameToDocument, isNonEmptyArray, argumentsObjectFromField, + canonicalStringify, } from "../../utilities/index.js"; import type { @@ -44,7 +45,6 @@ import type { StoreReader } from "./readFromStore.js"; import type { InMemoryCache } from "./inMemoryCache.js"; import type { EntityStore } from "./entityStore.js"; import type { Cache } from "../../core/index.js"; -import { canonicalStringify } from "./object-canon.js"; import { normalizeReadFieldOptions } from "./policies.js"; import type { ReadFieldFunction } from "../core/types/common.js"; diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 3443cfa08e8..55bae5b11a9 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -51,6 +51,11 @@ export interface ApolloClientOptions { */ uri?: string | UriFunction; credentials?: string; + /** + * An object representing headers to include in every HTTP request, such as `{Authorization: 'Bearer 1234'}` + * + * This value will be ignored when using the `link` option. + */ headers?: Record; /** * You can provide an {@link ApolloLink} instance to serve as Apollo Client's network layer. For more information, see [Advanced HTTP networking](https://www.apollographql.com/docs/react/networking/advanced-http-networking/). @@ -71,7 +76,7 @@ export interface ApolloClientOptions { */ ssrForceFetchDelay?: number; /** - * When using Apollo Client for [server-side rendering](https://www.apollographql.com/docs/react//performance/server-side-rendering/), set this to `true` so that the [`getDataFromTree` function](../react/ssr/#getdatafromtree) can work effectively. + * When using Apollo Client for [server-side rendering](https://www.apollographql.com/docs/react/performance/server-side-rendering/), set this to `true` so that the [`getDataFromTree` function](../react/ssr/#getdatafromtree) can work effectively. * * @defaultValue `false` */ @@ -94,6 +99,7 @@ export interface ApolloClientOptions { * See this [example object](https://www.apollographql.com/docs/react/api/core/ApolloClient#example-defaultoptions-object). */ defaultOptions?: DefaultOptions; + defaultContext?: Partial; /** * If `true`, Apollo Client will assume results read from the cache are never mutated by application code, which enables substantial performance optimizations. * @@ -121,6 +127,7 @@ export interface ApolloClientOptions { // @apollo/client/core. Since we need to preserve that API anyway, the easiest // solution is to reexport mergeOptions where it was previously declared (here). import { mergeOptions } from "../utilities/index.js"; +import { getApolloClientMemoryInternals } from "../utilities/caching/getMemoryInternals.js"; export { mergeOptions }; /** @@ -195,6 +202,7 @@ export class ApolloClient implements DataProxy { __DEV__, queryDeduplication = true, defaultOptions, + defaultContext, assumeImmutableResults = cache.assumeImmutableResults, resolvers, typeDefs, @@ -243,6 +251,7 @@ export class ApolloClient implements DataProxy { cache: this.cache, link: this.link, defaultOptions: this.defaultOptions, + defaultContext, documentTransform, queryDeduplication, ssrMode, @@ -739,4 +748,94 @@ export class ApolloClient implements DataProxy { public setLink(newLink: ApolloLink) { this.link = this.queryManager.link = newLink; } + + public get defaultContext() { + return this.queryManager.defaultContext; + } + + /** + * @experimental + * This is not a stable API - it is used in development builds to expose + * information to the DevTools. + * Use at your own risk! + * For more details, see [Memory Management](https://www.apollographql.com/docs/react/caching/memory-management/#measuring-cache-usage) + * + * @example + * ```ts + * console.log(client.getMemoryInternals()) + * ``` + * Logs output in the following JSON format: + * @example + * ```json + *{ + * limits: { + * parser: 1000, + * canonicalStringify: 1000, + * print: 2000, + * 'documentTransform.cache': 2000, + * 'queryManager.getDocumentInfo': 2000, + * 'PersistedQueryLink.persistedQueryHashes': 2000, + * 'fragmentRegistry.transform': 2000, + * 'fragmentRegistry.lookup': 1000, + * 'fragmentRegistry.findFragmentSpreads': 4000, + * 'cache.fragmentQueryDocuments': 1000, + * 'removeTypenameFromVariables.getVariableDefinitions': 2000, + * 'inMemoryCache.maybeBroadcastWatch': 5000, + * 'inMemoryCache.executeSelectionSet': 10000, + * 'inMemoryCache.executeSubSelectedArray': 5000 + * }, + * sizes: { + * parser: 26, + * canonicalStringify: 4, + * print: 14, + * addTypenameDocumentTransform: [ + * { + * cache: 14, + * }, + * ], + * queryManager: { + * getDocumentInfo: 14, + * documentTransforms: [ + * { + * cache: 14, + * }, + * { + * cache: 14, + * }, + * ], + * }, + * fragmentRegistry: { + * findFragmentSpreads: 34, + * lookup: 20, + * transform: 14, + * }, + * cache: { + * fragmentQueryDocuments: 22, + * }, + * inMemoryCache: { + * executeSelectionSet: 4345, + * executeSubSelectedArray: 1206, + * maybeBroadcastWatch: 32, + * }, + * links: [ + * { + * PersistedQueryLink: { + * persistedQueryHashes: 14, + * }, + * }, + * { + * removeTypenameFromVariables: { + * getVariableDefinitions: 14, + * }, + * }, + * ], + * }, + * } + *``` + */ + public getMemoryInternals?: typeof getApolloClientMemoryInternals; +} + +if (__DEV__) { + ApolloClient.prototype.getMemoryInternals = getApolloClientMemoryInternals; } diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 8516678dfc8..2b42f3b044e 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -80,6 +80,9 @@ export class ObservableQuery< // Computed shorthand for this.options.variables, preserved for // backwards compatibility. + /** + * An object containing the variables that were provided for the query. + */ public get variables(): TVariables | undefined { return this.options.variables; } @@ -225,6 +228,11 @@ export class ObservableQuery< }); } + /** @internal */ + public resetDiff() { + this.queryInfo.resetDiff(); + } + public getCurrentResult(saveAsLastResult = true): ApolloQueryResult { // Use the last result as long as the variables match this.variables. const lastResult = this.getLastResult(true); @@ -412,6 +420,9 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, return this.reobserve(reobserveOptions, NetworkStatus.refetch); } + /** + * A function that helps you fetch the next set of results for a [paginated list field](https://www.apollographql.com/docs/react/pagination/core-api/). + */ public fetchMore< TFetchData = TData, TFetchVars extends OperationVariables = TVariables, @@ -540,6 +551,11 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, // XXX the subscription variables are separate from the query variables. // if you want to update subscription variables, right now you have to do that separately, // and you can only do it by stopping the subscription and then subscribing again with new variables. + /** + * A function that enables you to execute a [subscription](https://www.apollographql.com/docs/react/data/subscriptions/), usually to subscribe to specific fields that were included in the query. + * + * This function returns _another_ function that you can call to terminate the subscription. + */ public subscribeToMore< TSubscriptionData = TData, TSubscriptionVariables extends OperationVariables = TVariables, @@ -645,6 +661,11 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, ); } + /** + * A function that enables you to update the query's cached result without executing a followup GraphQL operation. + * + * See [using updateQuery and updateFragment](https://www.apollographql.com/docs/react/caching/cache-interaction/#using-updatequery-and-updatefragment) for additional information. + */ public updateQuery( mapFn: ( previousQueryResult: TData, @@ -674,11 +695,17 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, } } + /** + * A function that instructs the query to begin re-executing at a specified interval (in milliseconds). + */ public startPolling(pollInterval: number) { this.options.pollInterval = pollInterval; this.updatePolling(); } + /** + * A function that instructs the query to stop polling after a previous call to `startPolling`. + */ public stopPolling() { this.options.pollInterval = 0; this.updatePolling(); @@ -776,7 +803,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, const maybeFetch = () => { if (this.pollingInfo) { - if (!isNetworkRequestInFlight(this.queryInfo.networkStatus)) { + if ( + !isNetworkRequestInFlight(this.queryInfo.networkStatus) && + !this.options.skipPollAttempt?.() + ) { this.reobserve( { // Most fetchPolicy options don't make sense to use in a polling context, as @@ -898,12 +928,16 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, const { concast, fromLink } = this.fetch(options, newNetworkStatus, query); const observer: Observer> = { next: (result) => { - finishWaitingForOwnResult(); - this.reportResult(result, variables); + if (equal(this.variables, variables)) { + finishWaitingForOwnResult(); + this.reportResult(result, variables); + } }, error: (error) => { - finishWaitingForOwnResult(); - this.reportError(error, variables); + if (equal(this.variables, variables)) { + finishWaitingForOwnResult(); + this.reportError(error, variables); + } }, }; diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index bd416b8b5be..f2aa2afa518 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -7,7 +7,7 @@ import { mergeIncrementalData } from "../utilities/index.js"; import type { WatchQueryOptions, ErrorPolicy } from "./watchQueryOptions.js"; import type { ObservableQuery } from "./ObservableQuery.js"; import { reobserveCacheFirst } from "./ObservableQuery.js"; -import type { QueryListener, MethodKeys } from "./types.js"; +import type { QueryListener } from "./types.js"; import type { FetchResult } from "../link/core/index.js"; import { isNonEmptyArray, @@ -36,10 +36,11 @@ const destructiveMethodCounts = new (canUseWeakMap ? WeakMap : Map)< function wrapDestructiveCacheMethod( cache: ApolloCache, - methodName: MethodKeys> + methodName: "evict" | "modify" | "reset" ) { const original = cache[methodName]; if (typeof original === "function") { + // @ts-expect-error this is just too generic to be typed correctly cache[methodName] = function () { destructiveMethodCounts.set( cache, @@ -156,6 +157,10 @@ export class QueryInfo { this.dirty = false; } + resetDiff() { + this.lastDiff = void 0; + } + getDiff(): Cache.DiffResult { const options = this.getDiffOptions(); @@ -359,7 +364,8 @@ export class QueryInfo { "variables" | "fetchPolicy" | "errorPolicy" >, cacheWriteBehavior: CacheWriteBehavior - ) { + ): typeof result { + result = { ...result }; const merger = new DeepMerger(); const graphQLErrors = isNonEmptyArray(result.errors) ? result.errors.slice(0) : []; @@ -405,7 +411,10 @@ export class QueryInfo { }); this.lastWrite = { - result, + // Make a shallow defensive copy of the result object, in case we + // later later modify result.data in place, since we don't want + // that mutation affecting the saved lastWrite.result.data. + result: { ...result }, variables: options.variables, dmCount: destructiveMethodCounts.get(this.cache), }; @@ -467,20 +476,19 @@ export class QueryInfo { this.updateWatch(options.variables); } - // If we're allowed to write to the cache, and we can read a - // complete result from the cache, update result.data to be the - // result from the cache, rather than the raw network result. - // Set without setDiff to avoid triggering a notify call, since - // we have other ways of notifying for this result. + // If we're allowed to write to the cache, update result.data to be + // the result as re-read from the cache, rather than the raw network + // result. Set without setDiff to avoid triggering a notify call, + // since we have other ways of notifying for this result. this.updateLastDiff(diff, diffOptions); - if (diff.complete) { - result.data = diff.result; - } + result.data = diff.result; }); } else { this.lastWrite = void 0; } } + + return result; } public markReady() { diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 62909a7f7b9..c8403d1420f 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -8,6 +8,7 @@ import { equal } from "@wry/equality"; import type { ApolloLink, FetchResult } from "../link/core/index.js"; import { execute } from "../link/core/index.js"; import { + defaultCacheSizes, hasDirectives, isExecutionPatchIncrementalResult, isExecutionPatchResult, @@ -27,7 +28,6 @@ import { hasClientExports, graphQLResultHasError, getGraphQLErrorsFromResult, - canUseWeakMap, Observable, asyncMap, isNonEmptyArray, @@ -62,6 +62,7 @@ import type { InternalRefetchQueriesOptions, InternalRefetchQueriesResult, InternalRefetchQueriesMap, + DefaultContext, } from "./types.js"; import { LocalState } from "./LocalState.js"; @@ -74,10 +75,13 @@ import { import type { ApolloErrorOptions } from "../errors/index.js"; import { PROTOCOL_ERRORS_SYMBOL } from "../errors/index.js"; import { print } from "../utilities/index.js"; +import type { IgnoreModifier } from "../cache/core/types/common.js"; import type { TODO } from "../utilities/types/TODO.js"; const { hasOwnProperty } = Object.prototype; +const IGNORE: IgnoreModifier = Object.create(null); + interface MutationStoreValue { mutation: DocumentNode; variables: Record; @@ -98,6 +102,8 @@ interface TransformCacheEntry { } import type { DefaultOptions } from "./ApolloClient.js"; +import { Trie } from "@wry/trie"; +import { AutoCleanedWeakCache, cacheSizes } from "../utilities/index.js"; export class QueryManager { public cache: ApolloCache; @@ -107,6 +113,7 @@ export class QueryManager { public readonly assumeImmutableResults: boolean; public readonly documentTransform: DocumentTransform; public readonly ssrMode: boolean; + public readonly defaultContext: Partial; private queryDeduplication: boolean; private clientAwareness: Record = {}; @@ -138,6 +145,7 @@ export class QueryManager { clientAwareness = {}, localState, assumeImmutableResults = !!cache.assumeImmutableResults, + defaultContext, }: { cache: ApolloCache; link: ApolloLink; @@ -149,6 +157,7 @@ export class QueryManager { clientAwareness?: Record; localState?: LocalState; assumeImmutableResults?: boolean; + defaultContext?: Partial; }) { const defaultDocumentTransform = new DocumentTransform( (document) => this.cache.transformDocument(document), @@ -174,10 +183,20 @@ export class QueryManager { // selections and fragments from the fragment registry. .concat(defaultDocumentTransform) : defaultDocumentTransform; + this.defaultContext = defaultContext || Object.create(null); if ((this.onBroadcast = onBroadcast)) { this.mutationStore = Object.create(null); } + + // TODO: remove before we release 3.9 + Object.defineProperty(this.inFlightLinkObservables, "get", { + value: () => { + throw new Error( + "This version of Apollo Client requires at least @apollo/experimental-nextjs-app-support version 0.5.2." + ); + }, + }); } /** @@ -253,7 +272,8 @@ export class QueryManager { error: null, } as MutationStoreValue); - if (optimisticResponse) { + const isOptimistic = + optimisticResponse && this.markMutationOptimistic( optimisticResponse, { @@ -268,7 +288,6 @@ export class QueryManager { keepRootFields, } ); - } this.broadcastQueries(); @@ -280,7 +299,7 @@ export class QueryManager { mutation, { ...context, - optimisticResponse, + optimisticResponse: isOptimistic ? optimisticResponse : void 0, }, variables, false @@ -320,7 +339,7 @@ export class QueryManager { updateQueries, awaitRefetchQueries, refetchQueries, - removeOptimistic: optimisticResponse ? mutationId : void 0, + removeOptimistic: isOptimistic ? mutationId : void 0, onQueryUpdated, keepRootFields, }); @@ -345,7 +364,7 @@ export class QueryManager { mutationStoreValue.error = err; } - if (optimisticResponse) { + if (isOptimistic) { self.cache.removeOptimistic(mutationId); } @@ -595,10 +614,14 @@ export class QueryManager { ) { const data = typeof optimisticResponse === "function" ? - optimisticResponse(mutation.variables) + optimisticResponse(mutation.variables, { IGNORE }) : optimisticResponse; - return this.cache.recordOptimisticTransaction((cache) => { + if (data === IGNORE) { + return false; + } + + this.cache.recordOptimisticTransaction((cache) => { try { this.markMutationResult( { @@ -611,6 +634,8 @@ export class QueryManager { invariant.error(error); } }, mutation.mutationId); + + return true; } public fetchQuery( @@ -647,10 +672,13 @@ export class QueryManager { return this.documentTransform.transformDocument(document); } - private transformCache = new (canUseWeakMap ? WeakMap : Map)< + private transformCache = new AutoCleanedWeakCache< DocumentNode, TransformCacheEntry - >(); + >( + cacheSizes["queryManager.getDocumentInfo"] || + defaultCacheSizes["queryManager.getDocumentInfo"] + ); public getDocumentInfo(document: DocumentNode) { const { transformCache } = this; @@ -1061,10 +1089,9 @@ export class QueryManager { // Use protected instead of private field so // @apollo/experimental-nextjs-app-support can access type info. - protected inFlightLinkObservables = new Map< - string, - Map> - >(); + protected inFlightLinkObservables = new Trie<{ + observable?: Observable>; + }>(false); private getObservableFromLink( query: DocumentNode, @@ -1074,7 +1101,7 @@ export class QueryManager { deduplication: boolean = context?.queryDeduplication ?? this.queryDeduplication ): Observable> { - let observable: Observable>; + let observable: Observable> | undefined; const { serverQuery, clientQuery } = this.getDocumentInfo(query); if (serverQuery) { @@ -1094,24 +1121,22 @@ export class QueryManager { if (deduplication) { const printedServerQuery = print(serverQuery); - const byVariables = - inFlightLinkObservables.get(printedServerQuery) || new Map(); - inFlightLinkObservables.set(printedServerQuery, byVariables); - const varJson = canonicalStringify(variables); - observable = byVariables.get(varJson); + const entry = inFlightLinkObservables.lookup( + printedServerQuery, + varJson + ); + + observable = entry.observable; if (!observable) { const concast = new Concast([ execute(link, operation) as Observable>, ]); - - byVariables.set(varJson, (observable = concast)); + observable = entry.observable = concast; concast.beforeNext(() => { - if (byVariables.delete(varJson) && byVariables.size < 1) { - inFlightLinkObservables.delete(printedServerQuery); - } + inFlightLinkObservables.remove(printedServerQuery, varJson); }); } } else { @@ -1178,7 +1203,7 @@ export class QueryManager { // Use linkDocument rather than queryInfo.document so the // operation/fragments used to write the result are the same as the // ones used to obtain it from the link. - queryInfo.markResult( + result = queryInfo.markResult( result, linkDocument, options, @@ -1671,6 +1696,7 @@ export class QueryManager { private prepareContext(context = {}) { const newContext = this.localState.prepareContext(context); return { + ...this.defaultContext, ...newContext, clientAwareness: this.clientAwareness, }; diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index add7b8a61ee..d25765e9c9b 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -34,6 +34,7 @@ import wrap from "../../testing/core/wrap"; import { resetStore } from "./QueryManager"; import { SubscriptionObserver } from "zen-observable-ts"; import { waitFor } from "@testing-library/react"; +import { ObservableStream } from "../../testing/internal"; export const mockFetchQuery = (queryManager: QueryManager) => { const fetchConcastWithInfo = queryManager["fetchConcastWithInfo"]; @@ -1086,6 +1087,98 @@ describe("ObservableQuery", () => { } ); + it("calling refetch with different variables before the query itself resolved will only yield the result for the new variables", async () => { + const observers: SubscriptionObserver>[] = []; + const queryManager = new QueryManager({ + cache: new InMemoryCache(), + link: new ApolloLink((operation, forward) => { + return new Observable((observer) => { + observers.push(observer); + }); + }), + }); + const observableQuery = queryManager.watchQuery({ + query, + variables: { id: 1 }, + }); + const stream = new ObservableStream(observableQuery); + + observableQuery.refetch({ id: 2 }); + + observers[0].next({ data: dataOne }); + observers[0].complete(); + + observers[1].next({ data: dataTwo }); + observers[1].complete(); + + { + const result = await stream.takeNext(); + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: dataTwo, + }); + } + expect(stream.take()).rejects.toThrow(/Timeout/i); + }); + + it("calling refetch multiple times with different variables will return only results for the most recent variables", async () => { + const observers: SubscriptionObserver>[] = []; + const queryManager = new QueryManager({ + cache: new InMemoryCache(), + link: new ApolloLink((operation, forward) => { + return new Observable((observer) => { + observers.push(observer); + }); + }), + }); + const observableQuery = queryManager.watchQuery({ + query, + variables: { id: 1 }, + }); + const stream = new ObservableStream(observableQuery); + + observers[0].next({ data: dataOne }); + observers[0].complete(); + + { + const result = await stream.takeNext(); + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: dataOne, + }); + } + + observableQuery.refetch({ id: 2 }); + observableQuery.refetch({ id: 3 }); + + observers[1].next({ data: dataTwo }); + observers[1].complete(); + + observers[2].next({ + data: { + people_one: { + name: "SomeOneElse", + }, + }, + }); + observers[2].complete(); + + { + const result = await stream.takeNext(); + expect(result).toEqual({ + loading: false, + networkStatus: NetworkStatus.ready, + data: { + people_one: { + name: "SomeOneElse", + }, + }, + }); + } + }); + itAsync( "calls fetchRequest with fetchPolicy `no-cache` when using `no-cache` fetch policy", (resolve, reject) => { diff --git a/src/core/__tests__/QueryManager/index.ts b/src/core/__tests__/QueryManager/index.ts index 083985c6e7c..9a958b7ccd8 100644 --- a/src/core/__tests__/QueryManager/index.ts +++ b/src/core/__tests__/QueryManager/index.ts @@ -47,6 +47,7 @@ import observableToPromise, { import { itAsync, subscribeAndCount } from "../../../testing/core"; import { ApolloClient } from "../../../core"; import { mockFetchQuery } from "../ObservableQuery"; +import { Concast, print } from "../../../utilities"; interface MockedMutation { reject: (reason: any) => any; @@ -66,14 +67,6 @@ export function resetStore(qm: QueryManager) { } describe("QueryManager", () => { - // Standard "get id from object" method. - const dataIdFromObject = (object: any) => { - if (object.__typename && object.id) { - return object.__typename + "__" + object.id; - } - return undefined; - }; - // Helper method that serves as the constructor method for // QueryManager but has defaults that make sense for these // tests. @@ -2224,107 +2217,6 @@ describe("QueryManager", () => { } ); - itAsync( - "should not return stale data when we orphan a real-id node in the store with a real-id node", - (resolve, reject) => { - const query1 = gql` - query { - author { - name { - firstName - lastName - } - age - id - __typename - } - } - `; - const query2 = gql` - query { - author { - name { - firstName - } - id - __typename - } - } - `; - const data1 = { - author: { - name: { - firstName: "John", - lastName: "Smith", - }, - age: 18, - id: "187", - __typename: "Author", - }, - }; - const data2 = { - author: { - name: { - firstName: "John", - }, - id: "197", - __typename: "Author", - }, - }; - const reducerConfig = { dataIdFromObject }; - const queryManager = createQueryManager({ - link: mockSingleLink( - { - request: { query: query1 }, - result: { data: data1 }, - }, - { - request: { query: query2 }, - result: { data: data2 }, - }, - { - request: { query: query1 }, - result: { data: data1 }, - } - ).setOnError(reject), - config: reducerConfig, - }); - - const observable1 = queryManager.watchQuery({ query: query1 }); - const observable2 = queryManager.watchQuery({ query: query2 }); - - // I'm not sure the waiting 60 here really is required, but the test used to do it - return Promise.all([ - observableToPromise( - { - observable: observable1, - wait: 60, - }, - (result) => { - expect(result).toEqual({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - observableToPromise( - { - observable: observable2, - wait: 60, - }, - (result) => { - expect(result).toEqual({ - data: data2, - loading: false, - networkStatus: NetworkStatus.ready, - }); - } - ), - ]).then(resolve, reject); - } - ); - itAsync( "should return partial data when configured when we orphan a real-id node in the store with a real-id node", (resolve, reject) => { @@ -2519,9 +2411,7 @@ describe("QueryManager", () => { loading: false, networkStatus: NetworkStatus.ready, data: { - info: { - a: "ay", - }, + info: {}, }, }); setTimeout(resolve, 100); @@ -6126,7 +6016,11 @@ describe("QueryManager", () => { queryManager.query({ query, context: { queryDeduplication: true } }); - expect(queryManager["inFlightLinkObservables"].size).toBe(1); + expect( + queryManager["inFlightLinkObservables"].peek(print(query), "{}") + ).toEqual({ + observable: expect.any(Concast), + }); }); it("should allow overriding global queryDeduplication: true to false", () => { @@ -6152,7 +6046,9 @@ describe("QueryManager", () => { queryManager.query({ query, context: { queryDeduplication: false } }); - expect(queryManager["inFlightLinkObservables"].size).toBe(0); + expect( + queryManager["inFlightLinkObservables"].peek(print(query), "{}") + ).toBeUndefined(); }); }); @@ -6252,4 +6148,229 @@ describe("QueryManager", () => { } ); }); + + describe("defaultContext", () => { + let _: any; // trash variable to throw away values when destructuring + _ = _; // omit "'_' is declared but its value is never read." compiler warning + + it("ApolloClient and QueryManager share a `defaultContext` instance (default empty object)", () => { + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + }); + + expect(client.defaultContext).toBe(client["queryManager"].defaultContext); + }); + + it("ApolloClient and QueryManager share a `defaultContext` instance (provided option)", () => { + const defaultContext = {}; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + defaultContext, + }); + + expect(client.defaultContext).toBe(defaultContext); + expect(client["queryManager"].defaultContext).toBe(defaultContext); + }); + + it("`defaultContext` cannot be reassigned on the user-facing `ApolloClient`", () => { + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: ApolloLink.empty(), + }); + + expect(() => { + // @ts-ignore + client.defaultContext = { query: { fetchPolicy: "cache-only" } }; + }).toThrowError(/Cannot set property defaultContext/); + }); + + it.each([ + ["query", { method: "query", option: "query" }], + ["mutation", { method: "mutate", option: "mutation" }], + ["subscription", { method: "subscribe", option: "query" }], + ] as const)( + "`defaultContext` will be applied to the context of a %s", + async (_, { method, option }) => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultContext: { + foo: "bar", + }, + }); + + // @ts-ignore a bit too generic for TS + client[method]({ + [option]: gql` + query { + foo + } + `, + }); + + expect(context.foo).toBe("bar"); + } + ); + + it("`ApolloClient.defaultContext` can be modified and changes will show up in future queries", async () => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultContext: { + foo: "bar", + }, + }); + + // one query to "warm up" with an old value to make sure the value + // isn't locked in at the first query or something + await client.query({ + query: gql` + query { + foo + } + `, + }); + + expect(context.foo).toBe("bar"); + + client.defaultContext.foo = "changed"; + + await client.query({ + query: gql` + query { + foo + } + `, + }); + + expect(context.foo).toBe("changed"); + }); + + it("`defaultContext` will be shallowly merged with explicit context", async () => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultContext: { + foo: { bar: "baz" }, + a: { b: "c" }, + }, + }); + + await client.query({ + query: gql` + query { + foo + } + `, + context: { + a: { x: "y" }, + }, + }); + + expect(context).toEqual( + expect.objectContaining({ + foo: { bar: "baz" }, + a: { b: undefined, x: "y" }, + }) + ); + }); + + it("`defaultContext` will be shallowly merged with context from `defaultOptions.query.context", async () => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultContext: { + foo: { bar: "baz" }, + a: { b: "c" }, + }, + defaultOptions: { + query: { + context: { + a: { x: "y" }, + }, + }, + }, + }); + + await client.query({ + query: gql` + query { + foo + } + `, + }); + + expect(context.foo).toStrictEqual({ bar: "baz" }); + expect(context.a).toStrictEqual({ x: "y" }); + }); + + it( + "document existing behavior: `defaultOptions.query.context` will be " + + "completely overwritten by, not merged with, explicit context", + async () => { + let context: any; + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink( + (operation) => + new Observable((observer) => { + ({ cache: _, ...context } = operation.getContext()); + observer.complete(); + }) + ), + defaultOptions: { + query: { + context: { + foo: { bar: "baz" }, + }, + }, + }, + }); + + await client.query({ + query: gql` + query { + foo + } + `, + context: { + a: { x: "y" }, + }, + }); + + expect(context.a).toStrictEqual({ x: "y" }); + expect(context.foo).toBeUndefined(); + } + ); + }); }); diff --git a/src/core/types.ts b/src/core/types.ts index eeda05fa41c..8085d013839 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -139,7 +139,7 @@ export type { QueryOptions as PureQueryOptions }; export type OperationVariables = Record; -export type ApolloQueryResult = { +export interface ApolloQueryResult { data: T; /** * A list of any errors that occurred during server-side execution of a GraphQL operation. @@ -158,7 +158,7 @@ export type ApolloQueryResult = { // result.partial will be true. Otherwise, result.partial will be falsy // (usually because the property is absent from the result object). partial?: boolean; -}; +} // This is part of the public API, people write these functions in `updateQueries`. export type MutationQueryReducer = ( @@ -174,7 +174,9 @@ export type MutationQueryReducersMap = { [queryName: string]: MutationQueryReducer; }; -// @deprecated Use MutationUpdaterFunction instead. +/** + * @deprecated Use `MutationUpdaterFunction` instead. + */ export type MutationUpdaterFn = ( // The MutationUpdaterFn type is broken because it mistakenly uses the same // type parameter T for both the cache and the mutationResult. Do not use this diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index b74659b4a99..5810c6464c4 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -12,6 +12,7 @@ import type { } from "./types.js"; import type { ApolloCache } from "../cache/index.js"; import type { ObservableQuery } from "./ObservableQuery.js"; +import type { IgnoreModifier } from "../cache/core/types/common.js"; /** * fetchPolicy determines where the client may return a result from. The options are: @@ -51,64 +52,34 @@ export type ErrorPolicy = "none" | "ignore" | "all"; * Query options. */ export interface QueryOptions { - /** - * A GraphQL document that consists of a single query to be sent down to the - * server. - */ - // TODO REFACTOR: rename this to document. Didn't do it yet because it's in a - // lot of tests. + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ query: DocumentNode | TypedDocumentNode; - /** - * A map going from variable name to variable value, where the variables are used - * within the GraphQL query. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables?: TVariables; - /** - * Specifies the {@link ErrorPolicy} to be used for this query - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** - * Context to be passed to link execution chain - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; - /** - * Specifies the {@link FetchPolicy} to be used for this query - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: FetchPolicy; - /** - * The time interval (in milliseconds) on which this query should be - * refetched from the server. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#pollInterval:member} */ pollInterval?: number; - /** - * Whether or not updates to the network status should trigger next on the observer of this query - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#notifyOnNetworkStatusChange:member} */ notifyOnNetworkStatusChange?: boolean; - /** - * Allow returning incomplete data from the cache when a larger query cannot - * be fully satisfied by the cache, instead of returning nothing. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#returnPartialData:member} */ returnPartialData?: boolean; - /** - * If `true`, perform a query `refetch` if the query result is marked as - * being partial, and the returned data is reset to an empty Object by the - * Apollo Client `QueryManager` (due to a cache miss). - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#partialRefetch:member} */ partialRefetch?: boolean; - /** - * Whether to canonize cache results before returning them. Canonization - * takes some extra time, but it speeds up future deep equality comparisons. - * Defaults to false. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ canonizeResults?: boolean; } @@ -118,15 +89,19 @@ export interface QueryOptions { export interface WatchQueryOptions< TVariables extends OperationVariables = OperationVariables, TData = any, +> extends SharedWatchQueryOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ + query: DocumentNode | TypedDocumentNode; +} + +export interface SharedWatchQueryOptions< + TVariables extends OperationVariables, + TData, > { - /** - * Specifies the {@link FetchPolicy} to be used for this query. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: WatchQueryFetchPolicy; - /** - * Specifies the {@link FetchPolicy} to be used after this query has completed. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#nextFetchPolicy:member} */ nextFetchPolicy?: | WatchQueryFetchPolicy | (( @@ -135,47 +110,38 @@ export interface WatchQueryOptions< context: NextFetchPolicyContext ) => WatchQueryFetchPolicy); - /** - * Defaults to the initial value of options.fetchPolicy, but can be explicitly - * configured to specify the WatchQueryFetchPolicy to revert back to whenever - * variables change (unless nextFetchPolicy intervenes). - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#initialFetchPolicy:member} */ initialFetchPolicy?: WatchQueryFetchPolicy; - /** - * Specifies whether a {@link NetworkStatus.refetch} operation should merge - * incoming field data with existing data, or overwrite the existing data. - * Overwriting is probably preferable, but merging is currently the default - * behavior, for backwards compatibility with Apollo Client 3.x. - */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#refetchWritePolicy:member} */ refetchWritePolicy?: RefetchWritePolicy; - /** {@inheritDoc @apollo/client!QueryOptions#query:member} */ - query: DocumentNode | TypedDocumentNode; - - /** {@inheritDoc @apollo/client!QueryOptions#variables:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables?: TVariables; - /** {@inheritDoc @apollo/client!QueryOptions#errorPolicy:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** {@inheritDoc @apollo/client!QueryOptions#context:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; - /** {@inheritDoc @apollo/client!QueryOptions#pollInterval:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#pollInterval:member} */ pollInterval?: number; - /** {@inheritDoc @apollo/client!QueryOptions#notifyOnNetworkStatusChange:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#notifyOnNetworkStatusChange:member} */ notifyOnNetworkStatusChange?: boolean; - /** {@inheritDoc @apollo/client!QueryOptions#returnPartialData:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#returnPartialData:member} */ returnPartialData?: boolean; - /** {@inheritDoc @apollo/client!QueryOptions#partialRefetch:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#partialRefetch:member} */ partialRefetch?: boolean; - /** {@inheritDoc @apollo/client!QueryOptions#canonizeResults:member} */ + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ canonizeResults?: boolean; + + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#skipPollAttempt:member} */ + skipPollAttempt?: () => boolean; } export interface NextFetchPolicyContext< @@ -189,7 +155,9 @@ export interface NextFetchPolicyContext< } export interface FetchMoreQueryOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ query?: DocumentNode | TypedDocumentNode; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables?: Partial; context?: DefaultContext; } @@ -224,31 +192,19 @@ export interface SubscriptionOptions< TVariables = OperationVariables, TData = any, > { - /** - * A GraphQL document, often created with `gql` from the `graphql-tag` - * package, that contains a single subscription inside of it. - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#query:member} */ query: DocumentNode | TypedDocumentNode; - /** - * An object that maps from the name of a variable as used in the subscription - * GraphQL document to that variable's value. - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#variables:member} */ variables?: TVariables; - /** - * Specifies the {@link FetchPolicy} to be used for this subscription. - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: FetchPolicy; - /** - * Specifies the {@link ErrorPolicy} to be used for this operation - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** - * Context object to be passed through the link execution chain. - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#context:member} */ context?: DefaultContext; } @@ -258,92 +214,35 @@ export interface MutationBaseOptions< TContext = DefaultContext, TCache extends ApolloCache = ApolloCache, > { - /** - * An object that represents the result of this mutation that will be - * optimistically stored before the server has actually returned a result. - * This is most often used for optimistic UI, where we want to be able to see - * the result of a mutation immediately, and update the UI later if any errors - * appear. - */ - optimisticResponse?: TData | ((vars: TVariables) => TData); - - /** - * A {@link MutationQueryReducersMap}, which is map from query names to - * mutation query reducers. Briefly, this map defines how to incorporate the - * results of the mutation into the results of queries that are currently - * being watched by your application. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#optimisticResponse:member} */ + optimisticResponse?: + | TData + | ((vars: TVariables, { IGNORE }: { IGNORE: IgnoreModifier }) => TData); + + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#updateQueries:member} */ updateQueries?: MutationQueryReducersMap; - /** - * A list of query names which will be refetched once this mutation has - * returned. This is often used if you have a set of queries which may be - * affected by a mutation and will have to update. Rather than writing a - * mutation query reducer (i.e. `updateQueries`) for this, you can simply - * refetch the queries that will be affected and achieve a consistent store - * once these queries return. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#refetchQueries:member} */ refetchQueries?: | ((result: FetchResult) => InternalRefetchQueriesInclude) | InternalRefetchQueriesInclude; - /** - * By default, `refetchQueries` does not wait for the refetched queries to - * be completed, before resolving the mutation `Promise`. This ensures that - * query refetching does not hold up mutation response handling (query - * refetching is handled asynchronously). Set `awaitRefetchQueries` to - * `true` if you would like to wait for the refetched queries to complete, - * before the mutation can be marked as resolved. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#awaitRefetchQueries:member} */ awaitRefetchQueries?: boolean; - /** - * A function which provides an {@link ApolloCache} instance, and the result - * of the mutation, to allow the user to update the store based on the - * results of the mutation. - * - * This function will be called twice over the lifecycle of a mutation. Once - * at the very beginning if an `optimisticResponse` was provided. The writes - * created from the optimistic data will be rolled back before the second time - * this function is called which is when the mutation has successfully - * resolved. At that point `update` will be called with the *actual* mutation - * result and those writes will not be rolled back. - * - * Note that since this function is intended to be used to update the - * store, it cannot be used with a `no-cache` fetch policy. If you're - * interested in performing some action after a mutation has completed, - * and you don't need to update the store, use the Promise returned from - * `client.mutate` instead. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#update:member} */ update?: MutationUpdaterFunction; - /** - * A function that will be called for each ObservableQuery affected by - * this mutation, after the mutation has completed. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#onQueryUpdated:member} */ onQueryUpdated?: OnQueryUpdated; - /** - * Specifies the {@link ErrorPolicy} to be used for this operation - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#errorPolicy:member} */ errorPolicy?: ErrorPolicy; - /** - * An object that maps from the name of a variable as used in the mutation - * GraphQL document to that variable's value. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#variables:member} */ variables?: TVariables; - /** - * The context to be passed to the link execution chain. This context will - * only be used with this mutation. It will not be used with - * `refetchQueries`. Refetched queries use the context they were - * initialized with (since the initial context is stored as part of the - * `ObservableQuery` instance). If a specific context is needed when - * refetching queries, make sure it is configured (via the - * [query `context` option](https://www.apollographql.com/docs/react/api/apollo-client#ApolloClient.query)) - * when the query is first initialized/run. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#context:member} */ context?: TContext; } @@ -352,28 +251,19 @@ export interface MutationOptions< TVariables = OperationVariables, TContext = DefaultContext, TCache extends ApolloCache = ApolloCache, -> extends MutationBaseOptions { - /** - * A GraphQL document, often created with `gql` from the `graphql-tag` - * package, that contains a single mutation inside of it. - */ +> extends MutationSharedOptions { + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#mutation:member} */ mutation: DocumentNode | TypedDocumentNode; - - /** - * Specifies the {@link MutationFetchPolicy} to be used for this query. - * Mutations support only 'network-only' and 'no-cache' fetchPolicy strings. - * If fetchPolicy is not provided, it defaults to 'network-only'. - */ +} +export interface MutationSharedOptions< + TData = any, + TVariables = OperationVariables, + TContext = DefaultContext, + TCache extends ApolloCache = ApolloCache, +> extends MutationBaseOptions { + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: MutationFetchPolicy; - /** - * To avoid retaining sensitive information from mutation root field - * arguments, Apollo Client v3.4+ automatically clears any `ROOT_MUTATION` - * fields from the cache after each mutation finishes. If you need this - * information to remain in the cache, you can prevent the removal by passing - * `keepRootFields: true` to the mutation. `ROOT_MUTATION` result data are - * also passed to the mutation `update` function, so we recommend obtaining - * the results that way, rather than using this option, if possible. - */ + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#keepRootFields:member} */ keepRootFields?: boolean; } diff --git a/src/link/batch-http/__tests__/batchHttpLink.ts b/src/link/batch-http/__tests__/batchHttpLink.ts index 544f44c304f..6dea1805b89 100644 --- a/src/link/batch-http/__tests__/batchHttpLink.ts +++ b/src/link/batch-http/__tests__/batchHttpLink.ts @@ -524,6 +524,9 @@ describe("SharedHttpTest", () => { expect(subscriber.next).toHaveBeenCalledTimes(2); expect(subscriber.complete).toHaveBeenCalledTimes(2); expect(subscriber.error).not.toHaveBeenCalled(); + // only one call because batchHttpLink can handle more than one subscriber + // without starting a new request + expect(fetchMock.calls().length).toBe(1); resolve(); }, 50); }); diff --git a/src/link/core/ApolloLink.ts b/src/link/core/ApolloLink.ts index ca9d2cfcd72..4f7759915d6 100644 --- a/src/link/core/ApolloLink.ts +++ b/src/link/core/ApolloLink.ts @@ -45,19 +45,21 @@ export class ApolloLink { const leftLink = toLink(left); const rightLink = toLink(right || new ApolloLink(passthrough)); + let ret: ApolloLink; if (isTerminating(leftLink) && isTerminating(rightLink)) { - return new ApolloLink((operation) => { + ret = new ApolloLink((operation) => { return test(operation) ? leftLink.request(operation) || Observable.of() : rightLink.request(operation) || Observable.of(); }); } else { - return new ApolloLink((operation, forward) => { + ret = new ApolloLink((operation, forward) => { return test(operation) ? leftLink.request(operation, forward) || Observable.of() : rightLink.request(operation, forward) || Observable.of(); }); } + return Object.assign(ret, { left: leftLink, right: rightLink }); } public static execute( @@ -88,8 +90,9 @@ export class ApolloLink { } const nextLink = toLink(second); + let ret: ApolloLink; if (isTerminating(nextLink)) { - return new ApolloLink( + ret = new ApolloLink( (operation) => firstLink.request( operation, @@ -97,7 +100,7 @@ export class ApolloLink { ) || Observable.of() ); } else { - return new ApolloLink((operation, forward) => { + ret = new ApolloLink((operation, forward) => { return ( firstLink.request(operation, (op) => { return nextLink.request(op, forward) || Observable.of(); @@ -105,6 +108,7 @@ export class ApolloLink { ); }); } + return Object.assign(ret, { left: firstLink, right: nextLink }); } constructor(request?: RequestHandler) { @@ -154,4 +158,21 @@ export class ApolloLink { this.onError = fn; return this; } + + /** + * @internal + * Used to iterate through all links that are concatenations or `split` links. + */ + readonly left?: ApolloLink; + /** + * @internal + * Used to iterate through all links that are concatenations or `split` links. + */ + readonly right?: ApolloLink; + + /** + * @internal + * Can be provided by a link that has an internal cache to report it's memory details. + */ + getMemoryInternals?: () => unknown; } diff --git a/src/link/http/__tests__/HttpLink.ts b/src/link/http/__tests__/HttpLink.ts index 6b29b63cfb2..31713defc3a 100644 --- a/src/link/http/__tests__/HttpLink.ts +++ b/src/link/http/__tests__/HttpLink.ts @@ -634,6 +634,7 @@ describe("HttpLink", () => { expect(subscriber.next).toHaveBeenCalledTimes(2); expect(subscriber.complete).toHaveBeenCalledTimes(2); expect(subscriber.error).not.toHaveBeenCalled(); + expect(fetchMock.calls().length).toBe(2); resolve(); }, 50); }); diff --git a/src/link/persisted-queries/__tests__/persisted-queries.test.ts b/src/link/persisted-queries/__tests__/persisted-queries.test.ts index ea8b56e660a..32d75fe5136 100644 --- a/src/link/persisted-queries/__tests__/persisted-queries.test.ts +++ b/src/link/persisted-queries/__tests__/persisted-queries.test.ts @@ -66,7 +66,7 @@ const giveUpResponse = JSON.stringify({ errors: giveUpErrors }); const giveUpResponseWithCode = JSON.stringify({ errors: giveUpErrorsWithCode }); const multiResponse = JSON.stringify({ errors: multipleErrors }); -export function sha256(data: string) { +function sha256(data: string) { const hash = crypto.createHash("sha256"); hash.update(data); return hash.digest("hex"); @@ -151,6 +151,32 @@ describe("happy path", () => { }, reject); }); + it("clears the cache when calling `resetHashCache`", async () => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); + + const hashRefs: WeakRef[] = []; + function hash(query: string) { + const newHash = new String(query); + hashRefs.push(new WeakRef(newHash)); + return newHash as string; + } + const persistedLink = createPersistedQuery({ sha256: hash }); + await new Promise((complete) => + execute(persistedLink.concat(createHttpLink()), { + query, + variables, + }).subscribe({ complete }) + ); + + await expect(hashRefs[0]).not.toBeGarbageCollected(); + persistedLink.resetHashCache(); + await expect(hashRefs[0]).toBeGarbageCollected(); + }); + itAsync("supports loading the hash from other method", (resolve, reject) => { fetchMock.post( "/graphql", @@ -517,6 +543,41 @@ describe("failure path", () => { }) ); + it.each([ + ["error message", giveUpResponse], + ["error code", giveUpResponseWithCode], + ] as const)( + "clears the cache when receiving NotSupported error (%s)", + async (_description, failingResponse) => { + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: failingResponse })), + { repeat: 1 } + ); + fetchMock.post( + "/graphql", + () => new Promise((resolve) => resolve({ body: response })), + { repeat: 1 } + ); + + const hashRefs: WeakRef[] = []; + function hash(query: string) { + const newHash = new String(query); + hashRefs.push(new WeakRef(newHash)); + return newHash as string; + } + const persistedLink = createPersistedQuery({ sha256: hash }); + await new Promise((complete) => + execute(persistedLink.concat(createHttpLink()), { + query, + variables, + }).subscribe({ complete }) + ); + + await expect(hashRefs[0]).toBeGarbageCollected(); + } + ); + itAsync("works with multiple errors", (resolve, reject) => { fetchMock.post( "/graphql", diff --git a/src/link/persisted-queries/__tests__/react.test.tsx b/src/link/persisted-queries/__tests__/react.test.tsx index 07c3fe7375e..b05e7d98f32 100644 --- a/src/link/persisted-queries/__tests__/react.test.tsx +++ b/src/link/persisted-queries/__tests__/react.test.tsx @@ -4,6 +4,7 @@ import * as ReactDOM from "react-dom/server"; import gql from "graphql-tag"; import { print } from "graphql"; import fetchMock from "fetch-mock"; +import crypto from "crypto"; import { ApolloProvider } from "../../../react/context"; import { InMemoryCache as Cache } from "../../../cache/inmemory/inMemoryCache"; @@ -12,7 +13,12 @@ import { createHttpLink } from "../../http/createHttpLink"; import { graphql } from "../../../react/hoc/graphql"; import { getDataFromTree } from "../../../react/ssr/getDataFromTree"; import { createPersistedQueryLink as createPersistedQuery, VERSION } from ".."; -import { sha256 } from "./persisted-queries.test"; + +function sha256(data: string) { + const hash = crypto.createHash("sha256"); + hash.update(data); + return hash.digest("hex"); +} // Necessary configuration in order to mock multiple requests // to a single (/graphql) endpoint diff --git a/src/link/persisted-queries/index.ts b/src/link/persisted-queries/index.ts index fa4c64bac58..920633bd7f3 100644 --- a/src/link/persisted-queries/index.ts +++ b/src/link/persisted-queries/index.ts @@ -12,6 +12,11 @@ import type { import { Observable, compact, isNonEmptyArray } from "../../utilities/index.js"; import type { NetworkError } from "../../errors/index.js"; import type { ServerError } from "../utils/index.js"; +import { + cacheSizes, + AutoCleanedWeakCache, + defaultCacheSizes, +} from "../../utilities/index.js"; export const VERSION = 1; @@ -93,7 +98,12 @@ function operationDefinesMutation(operation: Operation) { export const createPersistedQueryLink = ( options: PersistedQueryLink.Options ) => { - const hashesByQuery = new WeakMap>(); + let hashesByQuery: + | AutoCleanedWeakCache> + | undefined; + function resetHashCache() { + hashesByQuery = undefined; + } // Ensure a SHA-256 hash function is provided, if a custom hash // generation function is not provided. We don't supply a SHA-256 hash // function by default, to avoid forcing one as a dependency. Developers @@ -135,149 +145,175 @@ export const createPersistedQueryLink = ( // what to do with the bogus query. return getHashPromise(query); } + if (!hashesByQuery) { + hashesByQuery = new AutoCleanedWeakCache( + cacheSizes["PersistedQueryLink.persistedQueryHashes"] || + defaultCacheSizes["PersistedQueryLink.persistedQueryHashes"] + ); + } let hash = hashesByQuery.get(query)!; if (!hash) hashesByQuery.set(query, (hash = getHashPromise(query))); return hash; } - return new ApolloLink((operation, forward) => { - invariant( - forward, - "PersistedQueryLink cannot be the last link in the chain." - ); - - const { query } = operation; - - return new Observable((observer: Observer) => { - let subscription: ObservableSubscription; - let retried = false; - let originalFetchOptions: any; - let setFetchOptions = false; - const maybeRetry = ( - { - response, - networkError, - }: { response?: ExecutionResult; networkError?: ServerError }, - cb: () => void - ) => { - if (!retried && ((response && response.errors) || networkError)) { - retried = true; - - const graphQLErrors: GraphQLError[] = []; - - const responseErrors = response && response.errors; - if (isNonEmptyArray(responseErrors)) { - graphQLErrors.push(...responseErrors); - } - - // Network errors can return GraphQL errors on for example a 403 - let networkErrors; - if (typeof networkError?.result !== "string") { - networkErrors = - networkError && - networkError.result && - (networkError.result.errors as GraphQLError[]); - } - if (isNonEmptyArray(networkErrors)) { - graphQLErrors.push(...networkErrors); - } - - const disablePayload: ErrorResponse = { + return Object.assign( + new ApolloLink((operation, forward) => { + invariant( + forward, + "PersistedQueryLink cannot be the last link in the chain." + ); + + const { query } = operation; + + return new Observable((observer: Observer) => { + let subscription: ObservableSubscription; + let retried = false; + let originalFetchOptions: any; + let setFetchOptions = false; + const maybeRetry = ( + { response, networkError, - operation, - graphQLErrors: - isNonEmptyArray(graphQLErrors) ? graphQLErrors : void 0, - meta: processErrors(graphQLErrors), - }; + }: { response?: ExecutionResult; networkError?: ServerError }, + cb: () => void + ) => { + if (!retried && ((response && response.errors) || networkError)) { + retried = true; - // if the server doesn't support persisted queries, don't try anymore - supportsPersistedQueries = !disable(disablePayload); - - // if its not found, we can try it again, otherwise just report the error - if (retry(disablePayload)) { - // need to recall the link chain - if (subscription) subscription.unsubscribe(); - // actually send the query this time - operation.setContext({ - http: { - includeQuery: true, - includeExtensions: supportsPersistedQueries, - }, - fetchOptions: { - // Since we're including the full query, which may be - // large, we should send it in the body of a POST request. - // See issue #7456. - method: "POST", - }, - }); - if (setFetchOptions) { - operation.setContext({ fetchOptions: originalFetchOptions }); + const graphQLErrors: GraphQLError[] = []; + + const responseErrors = response && response.errors; + if (isNonEmptyArray(responseErrors)) { + graphQLErrors.push(...responseErrors); } - subscription = forward(operation).subscribe(handler); - return; - } - } - cb(); - }; - const handler = { - next: (response: ExecutionResult) => { - maybeRetry({ response }, () => observer.next!(response)); - }, - error: (networkError: ServerError) => { - maybeRetry({ networkError }, () => observer.error!(networkError)); - }, - complete: observer.complete!.bind(observer), - }; - - // don't send the query the first time - operation.setContext({ - http: { - includeQuery: !supportsPersistedQueries, - includeExtensions: supportsPersistedQueries, - }, - }); + // Network errors can return GraphQL errors on for example a 403 + let networkErrors; + if (typeof networkError?.result !== "string") { + networkErrors = + networkError && + networkError.result && + (networkError.result.errors as GraphQLError[]); + } + if (isNonEmptyArray(networkErrors)) { + graphQLErrors.push(...networkErrors); + } - // If requested, set method to GET if there are no mutations. Remember the - // original fetchOptions so we can restore them if we fall back to a - // non-hashed request. - if ( - useGETForHashedQueries && - supportsPersistedQueries && - !operationDefinesMutation(operation) - ) { - operation.setContext( - ({ fetchOptions = {} }: { fetchOptions: Record }) => { - originalFetchOptions = fetchOptions; - return { - fetchOptions: { - ...fetchOptions, - method: "GET", - }, + const disablePayload: ErrorResponse = { + response, + networkError, + operation, + graphQLErrors: + isNonEmptyArray(graphQLErrors) ? graphQLErrors : void 0, + meta: processErrors(graphQLErrors), }; + + // if the server doesn't support persisted queries, don't try anymore + supportsPersistedQueries = !disable(disablePayload); + if (!supportsPersistedQueries) { + // clear hashes from cache, we don't need them anymore + resetHashCache(); + } + + // if its not found, we can try it again, otherwise just report the error + if (retry(disablePayload)) { + // need to recall the link chain + if (subscription) subscription.unsubscribe(); + // actually send the query this time + operation.setContext({ + http: { + includeQuery: true, + includeExtensions: supportsPersistedQueries, + }, + fetchOptions: { + // Since we're including the full query, which may be + // large, we should send it in the body of a POST request. + // See issue #7456. + method: "POST", + }, + }); + if (setFetchOptions) { + operation.setContext({ fetchOptions: originalFetchOptions }); + } + subscription = forward(operation).subscribe(handler); + + return; + } } - ); - setFetchOptions = true; - } + cb(); + }; + const handler = { + next: (response: ExecutionResult) => { + maybeRetry({ response }, () => observer.next!(response)); + }, + error: (networkError: ServerError) => { + maybeRetry({ networkError }, () => observer.error!(networkError)); + }, + complete: observer.complete!.bind(observer), + }; + + // don't send the query the first time + operation.setContext({ + http: { + includeQuery: !supportsPersistedQueries, + includeExtensions: supportsPersistedQueries, + }, + }); + + // If requested, set method to GET if there are no mutations. Remember the + // original fetchOptions so we can restore them if we fall back to a + // non-hashed request. + if ( + useGETForHashedQueries && + supportsPersistedQueries && + !operationDefinesMutation(operation) + ) { + operation.setContext( + ({ fetchOptions = {} }: { fetchOptions: Record }) => { + originalFetchOptions = fetchOptions; + return { + fetchOptions: { + ...fetchOptions, + method: "GET", + }, + }; + } + ); + setFetchOptions = true; + } - if (supportsPersistedQueries) { - getQueryHash(query) - .then((sha256Hash) => { - operation.extensions.persistedQuery = { - version: VERSION, - sha256Hash, - }; - subscription = forward(operation).subscribe(handler); - }) - .catch(observer.error!.bind(observer)); - } else { - subscription = forward(operation).subscribe(handler); - } + if (supportsPersistedQueries) { + getQueryHash(query) + .then((sha256Hash) => { + operation.extensions.persistedQuery = { + version: VERSION, + sha256Hash, + }; + subscription = forward(operation).subscribe(handler); + }) + .catch(observer.error!.bind(observer)); + } else { + subscription = forward(operation).subscribe(handler); + } - return () => { - if (subscription) subscription.unsubscribe(); - }; - }); - }); + return () => { + if (subscription) subscription.unsubscribe(); + }; + }); + }), + { + resetHashCache, + }, + __DEV__ ? + { + getMemoryInternals() { + return { + PersistedQueryLink: { + persistedQueryHashes: hashesByQuery?.size ?? 0, + }, + }; + }, + } + : {} + ); }; diff --git a/src/link/remove-typename/removeTypenameFromVariables.ts b/src/link/remove-typename/removeTypenameFromVariables.ts index b8c173b15d2..31ed9c58a9c 100644 --- a/src/link/remove-typename/removeTypenameFromVariables.ts +++ b/src/link/remove-typename/removeTypenameFromVariables.ts @@ -2,8 +2,14 @@ import { wrap } from "optimism"; import type { DocumentNode, TypeNode } from "graphql"; import { Kind, visit } from "graphql"; import { ApolloLink } from "../core/index.js"; -import { stripTypename, isPlainObject } from "../../utilities/index.js"; +import { + stripTypename, + isPlainObject, + cacheSizes, + defaultCacheSizes, +} from "../../utilities/index.js"; import type { OperationVariables } from "../../core/index.js"; +import { WeakCache } from "@wry/caches"; export const KEEP = "__KEEP"; @@ -18,19 +24,32 @@ export interface RemoveTypenameFromVariablesOptions { export function removeTypenameFromVariables( options: RemoveTypenameFromVariablesOptions = Object.create(null) ) { - return new ApolloLink((operation, forward) => { - const { except } = options; - const { query, variables } = operation; - - if (variables) { - operation.variables = - except ? - maybeStripTypenameUsingConfig(query, variables, except) - : stripTypename(variables); - } - - return forward(operation); - }); + return Object.assign( + new ApolloLink((operation, forward) => { + const { except } = options; + const { query, variables } = operation; + + if (variables) { + operation.variables = + except ? + maybeStripTypenameUsingConfig(query, variables, except) + : stripTypename(variables); + } + + return forward(operation); + }), + __DEV__ ? + { + getMemoryInternals() { + return { + removeTypenameFromVariables: { + getVariableDefinitions: getVariableDefinitions?.size ?? 0, + }, + }; + }, + } + : {} + ); } function maybeStripTypenameUsingConfig( @@ -95,17 +114,25 @@ function maybeStripTypename( return value; } -const getVariableDefinitions = wrap((document: DocumentNode) => { - const definitions: Record = {}; +const getVariableDefinitions = wrap( + (document: DocumentNode) => { + const definitions: Record = {}; - visit(document, { - VariableDefinition(node) { - definitions[node.variable.name.value] = unwrapType(node.type); - }, - }); + visit(document, { + VariableDefinition(node) { + definitions[node.variable.name.value] = unwrapType(node.type); + }, + }); - return definitions; -}); + return definitions; + }, + { + max: + cacheSizes["removeTypenameFromVariables.getVariableDefinitions"] || + defaultCacheSizes["removeTypenameFromVariables.getVariableDefinitions"], + cache: WeakCache, + } +); function unwrapType(node: TypeNode): string { switch (node.kind) { diff --git a/src/link/retry/__tests__/retryLink.ts b/src/link/retry/__tests__/retryLink.ts index 3f5413e5b39..b9f3e14440d 100644 --- a/src/link/retry/__tests__/retryLink.ts +++ b/src/link/retry/__tests__/retryLink.ts @@ -92,7 +92,12 @@ describe("RetryLink", () => { expect(unsubscribeStub).toHaveBeenCalledTimes(1); }); - it("supports multiple subscribers to the same request", async () => { + it("multiple subscribers will trigger multiple requests", async () => { + const subscriber = { + next: jest.fn(console.log), + error: jest.fn(console.error), + complete: jest.fn(console.info), + }; const retry = new RetryLink({ delay: { initial: 1 }, attempts: { max: 5 }, @@ -102,13 +107,19 @@ describe("RetryLink", () => { stub.mockReturnValueOnce(fromError(standardError)); stub.mockReturnValueOnce(fromError(standardError)); stub.mockReturnValueOnce(Observable.of(data)); + stub.mockReturnValueOnce(fromError(standardError)); + stub.mockReturnValueOnce(fromError(standardError)); + stub.mockReturnValueOnce(Observable.of(data)); const link = ApolloLink.from([retry, stub]); const observable = execute(link, { query }); - const [result1, result2] = (await waitFor(observable, observable)) as any; - expect(result1.values).toEqual([data]); - expect(result2.values).toEqual([data]); - expect(stub).toHaveBeenCalledTimes(3); + observable.subscribe(subscriber); + observable.subscribe(subscriber); + await new Promise((resolve) => setTimeout(resolve, 3500)); + expect(subscriber.next).toHaveBeenNthCalledWith(1, data); + expect(subscriber.next).toHaveBeenNthCalledWith(2, data); + expect(subscriber.complete).toHaveBeenCalledTimes(2); + expect(stub).toHaveBeenCalledTimes(6); }); it("retries independently for concurrent requests", async () => { diff --git a/src/link/retry/retryLink.ts b/src/link/retry/retryLink.ts index d44a382500c..cde2dd2ea9c 100644 --- a/src/link/retry/retryLink.ts +++ b/src/link/retry/retryLink.ts @@ -1,14 +1,12 @@ import type { Operation, FetchResult, NextLink } from "../core/index.js"; import { ApolloLink } from "../core/index.js"; -import type { - Observer, - ObservableSubscription, -} from "../../utilities/index.js"; +import type { ObservableSubscription } from "../../utilities/index.js"; import { Observable } from "../../utilities/index.js"; import type { DelayFunction, DelayFunctionOptions } from "./delayFunction.js"; import { buildDelayFunction } from "./delayFunction.js"; import type { RetryFunction, RetryFunctionOptions } from "./retryFunction.js"; import { buildRetryFunction } from "./retryFunction.js"; +import type { SubscriptionObserver } from "zen-observable-ts"; export namespace RetryLink { export interface Options { @@ -27,78 +25,18 @@ export namespace RetryLink { /** * Tracking and management of operations that may be (or currently are) retried. */ -class RetryableOperation { +class RetryableOperation { private retryCount: number = 0; - private values: any[] = []; - private error: any; - private complete = false; - private canceled = false; - private observers: (Observer | null)[] = []; private currentSubscription: ObservableSubscription | null = null; private timerId: number | undefined; constructor( + private observer: SubscriptionObserver, private operation: Operation, - private nextLink: NextLink, + private forward: NextLink, private delayFor: DelayFunction, private retryIf: RetryFunction - ) {} - - /** - * Register a new observer for this operation. - * - * If the operation has previously emitted other events, they will be - * immediately triggered for the observer. - */ - public subscribe(observer: Observer) { - if (this.canceled) { - throw new Error( - `Subscribing to a retryable link that was canceled is not supported` - ); - } - this.observers.push(observer); - - // If we've already begun, catch this observer up. - for (const value of this.values) { - observer.next!(value); - } - - if (this.complete) { - observer.complete!(); - } else if (this.error) { - observer.error!(this.error); - } - } - - /** - * Remove a previously registered observer from this operation. - * - * If no observers remain, the operation will stop retrying, and unsubscribe - * from its downstream link. - */ - public unsubscribe(observer: Observer) { - const index = this.observers.indexOf(observer); - if (index < 0) { - throw new Error( - `RetryLink BUG! Attempting to unsubscribe unknown observer!` - ); - } - // Note that we are careful not to change the order of length of the array, - // as we are often mid-iteration when calling this method. - this.observers[index] = null; - - // If this is the last observer, we're done. - if (this.observers.every((o) => o === null)) { - this.cancel(); - } - } - - /** - * Start the initial request. - */ - public start() { - if (this.currentSubscription) return; // Already started. - + ) { this.try(); } @@ -112,33 +50,16 @@ class RetryableOperation { clearTimeout(this.timerId); this.timerId = undefined; this.currentSubscription = null; - this.canceled = true; } private try() { - this.currentSubscription = this.nextLink(this.operation).subscribe({ - next: this.onNext, + this.currentSubscription = this.forward(this.operation).subscribe({ + next: this.observer.next.bind(this.observer), error: this.onError, - complete: this.onComplete, + complete: this.observer.complete.bind(this.observer), }); } - private onNext = (value: any) => { - this.values.push(value); - for (const observer of this.observers) { - if (!observer) continue; - observer.next!(value); - } - }; - - private onComplete = () => { - this.complete = true; - for (const observer of this.observers) { - if (!observer) continue; - observer.complete!(); - } - }; - private onError = async (error: any) => { this.retryCount += 1; @@ -153,11 +74,7 @@ class RetryableOperation { return; } - this.error = error; - for (const observer of this.observers) { - if (!observer) continue; - observer.error!(error); - } + this.observer.error(error); }; private scheduleRetry(delay: number) { @@ -189,18 +106,16 @@ export class RetryLink extends ApolloLink { operation: Operation, nextLink: NextLink ): Observable { - const retryable = new RetryableOperation( - operation, - nextLink, - this.delayFor, - this.retryIf - ); - retryable.start(); - return new Observable((observer) => { - retryable.subscribe(observer); + const retryable = new RetryableOperation( + observer, + operation, + nextLink, + this.delayFor, + this.retryIf + ); return () => { - retryable.unsubscribe(observer); + retryable.cancel(); }; }); } 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/components/Mutation.tsx b/src/react/components/Mutation.tsx index 492e0089312..ff122a7e648 100644 --- a/src/react/components/Mutation.tsx +++ b/src/react/components/Mutation.tsx @@ -1,12 +1,19 @@ import * as PropTypes from "prop-types"; +import type * as ReactTypes from "react"; import type { OperationVariables } from "../../core/index.js"; import type { MutationComponentOptions } from "./types.js"; import { useMutation } from "../hooks/index.js"; +/** + * @deprecated + * Official support for React Apollo render prop components ended in March 2020. + * This library is still included in the `@apollo/client` package, + * but it no longer receives feature updates or bug fixes. + */ export function Mutation( props: MutationComponentOptions -) { +): ReactTypes.JSX.Element | null { const [runMutation, result] = useMutation(props.mutation, props); return props.children ? props.children(runMutation, result) : null; } diff --git a/src/react/components/Query.tsx b/src/react/components/Query.tsx index 1e5611f646a..428207784c7 100644 --- a/src/react/components/Query.tsx +++ b/src/react/components/Query.tsx @@ -1,13 +1,22 @@ import * as PropTypes from "prop-types"; +import type * as ReactTypes from "react"; import type { OperationVariables } from "../../core/index.js"; import type { QueryComponentOptions } from "./types.js"; import { useQuery } from "../hooks/index.js"; +/** + * @deprecated + * Official support for React Apollo render prop components ended in March 2020. + * This library is still included in the `@apollo/client` package, + * but it no longer receives feature updates or bug fixes. + */ export function Query< TData = any, TVariables extends OperationVariables = OperationVariables, ->(props: QueryComponentOptions) { +>( + props: QueryComponentOptions +): ReactTypes.JSX.Element | null { const { children, query, ...options } = props; const result = useQuery(query, options); return result ? children(result as any) : null; diff --git a/src/react/components/Subscription.tsx b/src/react/components/Subscription.tsx index 5701cdcb01d..59d694156a5 100644 --- a/src/react/components/Subscription.tsx +++ b/src/react/components/Subscription.tsx @@ -1,13 +1,22 @@ import * as PropTypes from "prop-types"; +import type * as ReactTypes from "react"; import type { OperationVariables } from "../../core/index.js"; import type { SubscriptionComponentOptions } from "./types.js"; import { useSubscription } from "../hooks/index.js"; +/** + * @deprecated + * Official support for React Apollo render prop components ended in March 2020. + * This library is still included in the `@apollo/client` package, + * but it no longer receives feature updates or bug fixes. + */ export function Subscription< TData = any, TVariables extends OperationVariables = OperationVariables, ->(props: SubscriptionComponentOptions) { +>( + props: SubscriptionComponentOptions +): ReactTypes.JSX.Element | null { const result = useSubscription(props.subscription, props); return props.children && result ? props.children(result) : null; } diff --git a/src/react/components/types.ts b/src/react/components/types.ts index 4e1abacb6a1..a742b905ac6 100644 --- a/src/react/components/types.ts +++ b/src/react/components/types.ts @@ -1,6 +1,8 @@ import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; +import type * as ReactTypes from "react"; + import type { OperationVariables, DefaultContext, @@ -20,7 +22,9 @@ export interface QueryComponentOptions< TData = any, TVariables extends OperationVariables = OperationVariables, > extends QueryFunctionOptions { - children: (result: QueryResult) => JSX.Element | null; + children: ( + result: QueryResult + ) => ReactTypes.JSX.Element | null; query: DocumentNode | TypedDocumentNode; } @@ -34,13 +38,16 @@ export interface MutationComponentOptions< children: ( mutateFunction: MutationFunction, result: MutationResult - ) => JSX.Element | null; + ) => ReactTypes.JSX.Element | null; } export interface SubscriptionComponentOptions< TData = any, TVariables extends OperationVariables = OperationVariables, > extends BaseSubscriptionOptions { + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#query:member} */ subscription: DocumentNode | TypedDocumentNode; - children?: null | ((result: SubscriptionResult) => JSX.Element | null); + children?: + | null + | ((result: SubscriptionResult) => ReactTypes.JSX.Element | null); } diff --git a/src/react/context/ApolloConsumer.tsx b/src/react/context/ApolloConsumer.tsx index ac26e734da9..e71ec520ee3 100644 --- a/src/react/context/ApolloConsumer.tsx +++ b/src/react/context/ApolloConsumer.tsx @@ -1,15 +1,16 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { ApolloClient } from "../../core/index.js"; import { getApolloContext } from "./ApolloContext.js"; export interface ApolloConsumerProps { - children: (client: ApolloClient) => React.ReactChild | null; + children: (client: ApolloClient) => ReactTypes.ReactChild | null; } -export const ApolloConsumer: React.FC = (props) => { +export const ApolloConsumer: ReactTypes.FC = (props) => { const ApolloContext = getApolloContext(); return ( diff --git a/src/react/context/ApolloContext.ts b/src/react/context/ApolloContext.ts index de6203aaadc..e0646a43431 100644 --- a/src/react/context/ApolloContext.ts +++ b/src/react/context/ApolloContext.ts @@ -1,4 +1,5 @@ -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { ApolloClient } from "../../core/index.js"; import { canUseSymbol } from "../../utilities/index.js"; import type { RenderPromises } from "../ssr/index.js"; @@ -16,7 +17,7 @@ export interface ApolloContextValue { const contextKey = canUseSymbol ? Symbol.for("__APOLLO_CONTEXT__") : "__APOLLO_CONTEXT__"; -export function getApolloContext(): React.Context { +export function getApolloContext(): ReactTypes.Context { invariant( "createContext" in React, "Invoking `getApolloContext` in an environment where `React.createContext` is not available.\n" + diff --git a/src/react/context/ApolloProvider.tsx b/src/react/context/ApolloProvider.tsx index 930e32dab0a..aa91b2cfc01 100644 --- a/src/react/context/ApolloProvider.tsx +++ b/src/react/context/ApolloProvider.tsx @@ -1,16 +1,17 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { ApolloClient } from "../../core/index.js"; import { getApolloContext } from "./ApolloContext.js"; export interface ApolloProviderProps { client: ApolloClient; - children: React.ReactNode | React.ReactNode[] | null; + children: ReactTypes.ReactNode | ReactTypes.ReactNode[] | null; } -export const ApolloProvider: React.FC> = ({ +export const ApolloProvider: ReactTypes.FC> = ({ client, children, }) => { diff --git a/src/react/hoc/graphql.tsx b/src/react/hoc/graphql.tsx index 88bf39e9961..181bb5a951e 100644 --- a/src/react/hoc/graphql.tsx +++ b/src/react/hoc/graphql.tsx @@ -1,4 +1,5 @@ import type { DocumentNode } from "graphql"; +import type * as ReactTypes from "react"; import { parser, DocumentType } from "../parser/index.js"; import { withQuery } from "./query-hoc.js"; @@ -7,6 +8,11 @@ import { withSubscription } from "./subscription-hoc.js"; import type { OperationOption, DataProps, MutateProps } from "./types.js"; import type { OperationVariables } from "../../core/index.js"; +/** + * @deprecated + * Official support for React Apollo higher order components ended in March 2020. + * This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. + */ export function graphql< TProps extends TGraphQLVariables | {} = {}, TData extends object = {}, @@ -22,8 +28,8 @@ export function graphql< TChildProps > = {} ): ( - WrappedComponent: React.ComponentType -) => React.ComponentClass { + WrappedComponent: ReactTypes.ComponentType +) => ReactTypes.ComponentClass { switch (parser(document).type) { case DocumentType.Mutation: return withMutation(document, operationOptions); diff --git a/src/react/hoc/hoc-utils.tsx b/src/react/hoc/hoc-utils.tsx index 2e59f74e944..7c7d0598e08 100644 --- a/src/react/hoc/hoc-utils.tsx +++ b/src/react/hoc/hoc-utils.tsx @@ -1,5 +1,5 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; import type { OperationVariables } from "../../core/index.js"; import type { IDocumentDefinition } from "../parser/index.js"; diff --git a/src/react/hoc/mutation-hoc.tsx b/src/react/hoc/mutation-hoc.tsx index 8d4162eeee0..2620f46ff81 100644 --- a/src/react/hoc/mutation-hoc.tsx +++ b/src/react/hoc/mutation-hoc.tsx @@ -1,4 +1,5 @@ -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { DocumentNode } from "graphql"; import hoistNonReactStatics from "hoist-non-react-statics"; @@ -20,6 +21,11 @@ import { import type { OperationOption, OptionProps, MutateProps } from "./types.js"; import type { ApolloCache } from "../../core/index.js"; +/** + * @deprecated + * Official support for React Apollo higher order components ended in March 2020. + * This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. + */ export function withMutation< TProps extends TGraphQLVariables | {} = {}, TData extends Record = {}, @@ -56,8 +62,8 @@ export function withMutation< >; return ( - WrappedComponent: React.ComponentType - ): React.ComponentClass => { + WrappedComponent: ReactTypes.ComponentType + ): ReactTypes.ComponentClass => { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; class GraphQL extends GraphQLBase { static displayName = graphQLDisplayName; diff --git a/src/react/hoc/query-hoc.tsx b/src/react/hoc/query-hoc.tsx index cccabd9722f..cdefd397916 100644 --- a/src/react/hoc/query-hoc.tsx +++ b/src/react/hoc/query-hoc.tsx @@ -1,4 +1,5 @@ -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { DocumentNode } from "graphql"; import hoistNonReactStatics from "hoist-non-react-statics"; @@ -14,6 +15,11 @@ import { } from "./hoc-utils.js"; import type { OperationOption, OptionProps, DataProps } from "./types.js"; +/** + * @deprecated + * Official support for React Apollo higher order components ended in March 2020. + * This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. + */ export function withQuery< TProps extends TGraphQLVariables | Record = Record, TData extends object = {}, @@ -50,8 +56,8 @@ export function withQuery< // allow for advanced referential equality checks let lastResultProps: TChildProps | void; return ( - WrappedComponent: React.ComponentType - ): React.ComponentClass => { + WrappedComponent: ReactTypes.ComponentType + ): ReactTypes.ComponentClass => { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; class GraphQL extends GraphQLBase { static displayName = graphQLDisplayName; diff --git a/src/react/hoc/subscription-hoc.tsx b/src/react/hoc/subscription-hoc.tsx index fd3d035599b..bb11060aaf7 100644 --- a/src/react/hoc/subscription-hoc.tsx +++ b/src/react/hoc/subscription-hoc.tsx @@ -1,4 +1,5 @@ -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import type { DocumentNode } from "graphql"; import hoistNonReactStatics from "hoist-non-react-statics"; @@ -14,6 +15,11 @@ import { } from "./hoc-utils.js"; import type { OperationOption, OptionProps, DataProps } from "./types.js"; +/** + * @deprecated + * Official support for React Apollo higher order components ended in March 2020. + * This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. + */ export function withSubscription< TProps extends TGraphQLVariables | {} = {}, TData extends object = {}, @@ -48,8 +54,8 @@ export function withSubscription< // allow for advanced referential equality checks let lastResultProps: TChildProps | void; return ( - WrappedComponent: React.ComponentType - ): React.ComponentClass => { + WrappedComponent: ReactTypes.ComponentType + ): ReactTypes.ComponentClass => { const graphQLDisplayName = `${alias}(${getDisplayName(WrappedComponent)})`; class GraphQL extends GraphQLBase< TProps, diff --git a/src/react/hoc/withApollo.tsx b/src/react/hoc/withApollo.tsx index 3ec1e39ec29..54cb7c67be9 100644 --- a/src/react/hoc/withApollo.tsx +++ b/src/react/hoc/withApollo.tsx @@ -1,20 +1,26 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import hoistNonReactStatics from "hoist-non-react-statics"; import { ApolloConsumer } from "../context/index.js"; import type { OperationOption, WithApolloClient } from "./types.js"; -function getDisplayName

(WrappedComponent: React.ComponentType

) { +function getDisplayName

(WrappedComponent: ReactTypes.ComponentType

) { return WrappedComponent.displayName || WrappedComponent.name || "Component"; } +/** + * @deprecated + * Official support for React Apollo higher order components ended in March 2020. + * This library is still included in the `@apollo/client` package, but it no longer receives feature updates or bug fixes. + */ export function withApollo( - WrappedComponent: React.ComponentType< + WrappedComponent: ReactTypes.ComponentType< WithApolloClient> >, operationOptions: OperationOption = {} -): React.ComponentClass> { +): ReactTypes.ComponentClass> { const withDisplayName = `withApollo(${getDisplayName(WrappedComponent)})`; class WithApollo extends React.Component> { @@ -39,7 +45,9 @@ export function withApollo( return this.wrappedInstance; } - setWrappedInstance(ref: React.ComponentType>) { + setWrappedInstance( + ref: ReactTypes.ComponentType> + ) { this.wrappedInstance = ref; } diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index 32bd0edcdc0..fbd7c3dd973 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -1,2747 +1,2431 @@ -import React, { ComponentProps, Fragment, StrictMode, Suspense } from "react"; -import { - act, - render, - screen, - screen as _screen, - renderHook, - RenderHookOptions, - waitFor, -} from "@testing-library/react"; +import React, { Suspense } from "react"; +import { act, screen, renderHook } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { ErrorBoundary, ErrorBoundaryProps } from "react-error-boundary"; +import { + ErrorBoundary as ReactErrorBoundary, + FallbackProps, +} from "react-error-boundary"; import { expectTypeOf } from "expect-type"; import { GraphQLError } from "graphql"; import { gql, ApolloError, - DocumentNode, ApolloClient, ErrorPolicy, - NormalizedCacheObject, NetworkStatus, - ApolloCache, TypedDocumentNode, ApolloLink, Observable, - FetchMoreQueryOptions, - OperationVariables, - ApolloQueryResult, } from "../../../core"; import { MockedResponse, - MockedProvider, MockLink, MockSubscriptionLink, mockSingleLink, + MockedProvider, + wait, } from "../../../testing"; import { concatPagination, offsetLimitPagination, DeepPartial, - cloneDeep, } from "../../../utilities"; import { useBackgroundQuery } from "../useBackgroundQuery"; -import { useReadQuery } from "../useReadQuery"; +import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; import { ApolloProvider } from "../../context"; -import { unwrapQueryRef, QueryReference } from "../../cache/QueryReference"; +import { QueryReference } from "../../internal"; import { InMemoryCache } from "../../../cache"; -import { - SuspenseQueryHookFetchPolicy, - SuspenseQueryHookOptions, -} from "../../types/types"; +import { SuspenseQueryHookFetchPolicy } from "../../types/types"; import equal from "@wry/equality"; import { RefetchWritePolicy } from "../../../core/watchQueryOptions"; import { skipToken } from "../constants"; -import { profile, spyOnConsole } from "../../../testing/internal"; - -function renderIntegrationTest({ - client, -}: { - client?: ApolloClient; -} = {}) { - const query: TypedDocumentNode = gql` - query SimpleQuery { - foo { - bar - } - } - `; - - const mocks = [ - { - request: { query }, - result: { data: { foo: { bar: "hello" } } }, - }, - ]; - const _client = - client || - new ApolloClient({ - cache: new InMemoryCache(), - link: new MockLink(mocks), - }); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - }; - const errorBoundaryProps: ErrorBoundaryProps = { - fallback:

Error
, - onError: (error) => { - renders.errorCount++; - renders.errors.push(error); - }, - }; - - interface QueryData { - foo: { bar: string }; - } +import { + PaginatedCaseData, + Profiler, + SimpleCaseData, + VariablesCaseData, + VariablesCaseVariables, + createProfiler, + renderWithClient, + renderWithMocks, + setupPaginatedCase, + setupSimpleCase, + setupVariablesCase, + spyOnConsole, + useTrackRenders, +} from "../../../testing/internal"; + +afterEach(() => { + jest.useRealTimers(); +}); +function createDefaultTrackedComponents< + Snapshot extends { result: UseReadQueryResult | null }, + TData = Snapshot["result"] extends UseReadQueryResult | null ? + TData + : unknown, +>(Profiler: Profiler) { function SuspenseFallback() { - renders.suspenseCount++; - return
loading
; + useTrackRenders(); + return
Loading
; } - function Child({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); - // count renders in the child component - renders.count++; - return
{data.foo.bar}
; - } + function ReadQueryHook({ queryRef }: { queryRef: QueryReference }) { + useTrackRenders(); + Profiler.mergeSnapshot({ + result: useReadQuery(queryRef), + } as Partial); - function Parent() { - const [queryRef] = useBackgroundQuery(query); - return ; + return null; } - function ParentWithVariables() { - const [queryRef] = useBackgroundQuery(query); - return ; + return { SuspenseFallback, ReadQueryHook }; +} + +function createTrackedErrorComponents( + Profiler: Profiler +) { + function ErrorFallback({ error }: FallbackProps) { + useTrackRenders({ name: "ErrorFallback" }); + Profiler.mergeSnapshot({ error } as Partial); + + return
Error
; } - function App({ variables }: { variables?: Record }) { + function ErrorBoundary({ children }: { children: React.ReactNode }) { return ( - - - }> - {variables ? - - : } - - - + + {children} + ); } - const { ...rest } = render(); - return { ...rest, query, client: _client, renders }; + return { ErrorBoundary }; } -interface VariablesCaseData { - character: { - id: string; - name: string; - }; +function createErrorProfiler() { + return createProfiler({ + initialSnapshot: { + error: null as Error | null, + result: null as UseReadQueryResult | null, + }, + }); } - -interface VariablesCaseVariables { - id: string; +function createDefaultProfiler() { + return createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); } -function useVariablesIntegrationTestCase() { - const query: TypedDocumentNode = - gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const CHARACTERS = ["Spider-Man", "Black Widow", "Iron Man", "Hulk"]; - let mocks = [...CHARACTERS].map((name, index) => ({ - request: { query, variables: { id: String(index + 1) } }, - result: { data: { character: { id: String(index + 1), name } } }, - })); - return { mocks, query }; -} +it("fetches a simple query with minimal config", async () => { + const { query, mocks } = setupSimpleCase(); -function renderVariablesIntegrationTest({ - variables, - mocks, - errorPolicy, - options, - cache, -}: { - mocks?: { - request: { query: DocumentNode; variables: { id: string } }; - result: { - data?: { - character: { - id: string; - name: string | null; - }; - }; - }; - }[]; - variables: { id: string }; - options?: SuspenseQueryHookOptions; - cache?: InMemoryCache; - errorPolicy?: ErrorPolicy; -}) { - let { mocks: _mocks, query } = useVariablesIntegrationTestCase(); - - // duplicate mocks with (updated) in the name for refetches - _mocks = [..._mocks, ..._mocks, ..._mocks].map( - ({ request, result }, index) => { - return { - request: request, - result: { - data: { - character: { - ...result.data.character, - name: - index > 3 ? - index > 7 ? - `${result.data.character.name} (updated again)` - : `${result.data.character.name} (updated)` - : result.data.character.name, - }, - }, - }, - }; - } - ); - const client = new ApolloClient({ - cache: cache || new InMemoryCache(), - link: new MockLink(mocks || _mocks), - }); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: VariablesCaseData; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + const Profiler = createDefaultProfiler(); - const errorBoundaryProps: ErrorBoundaryProps = { - fallback:
Error
, - onError: (error) => { - renders.errorCount++; - renders.errors.push(error); - }, - }; + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function SuspenseFallback() { - renders.suspenseCount++; - return
loading
; - } - - function Child({ - refetch, - variables: _variables, - queryRef, - }: { - variables: VariablesCaseVariables; - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); - const [variables, setVariables] = React.useState(_variables); - // count renders in the child component - renders.count++; - renders.frames.push({ data, networkStatus, error }); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); return ( -
- {error ? -
{error.message}
- : null} - - - {data?.character.id} - {data?.character.name} -
+ }> + + ); } - function ParentWithVariables({ - variables, - errorPolicy = "none", - }: { - variables: VariablesCaseVariables; - errorPolicy?: ErrorPolicy; - }) { - const [queryRef, { refetch }] = useBackgroundQuery(query, { - ...options, - variables, - errorPolicy, + renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - return ( - - ); } - function App({ - variables, - errorPolicy, - }: { - variables: VariablesCaseVariables; - errorPolicy?: ErrorPolicy; - }) { + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it("tears down the query on unmount", async () => { + const { query, mocks } = setupSimpleCase(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); + return ( - - - }> - - - - + }> + + ); } - const ProfiledApp = profile>({ - Component: App, - snapshotDOM: true, - onRender: ({ replaceSnapshot }) => replaceSnapshot(cloneDeep(renders)), - }); + const { unmount } = renderWithClient(, { client, wrapper: Profiler }); - const { ...rest } = render( - - ); - const rerender = ({ variables }: { variables: VariablesCaseVariables }) => { - return rest.rerender(); - }; - return { - ...rest, - ProfiledApp, - query, - rerender, - client, - renders, - mocks: mocks || _mocks, - }; -} + // initial suspended render + await Profiler.takeRender(); -function renderPaginatedIntegrationTest({ - updateQuery, - fieldPolicies, -}: { - fieldPolicies?: boolean; - updateQuery?: boolean; - mocks?: { - request: { - query: DocumentNode; - variables: { offset: number; limit: number }; - }; - result: { - data: { - letters: { - letter: string; - position: number; - }[]; - }; - }; - }[]; -} = {}) { - interface QueryData { - letters: { - letter: string; - position: number; - }[]; - } + { + const { snapshot } = await Profiler.takeRender(); - interface Variables { - limit?: number; - offset?: number; + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); } - const query: TypedDocumentNode = gql` - query letters($limit: Int, $offset: Int) { - letters(limit: $limit) { - letter - position - } - } - `; + unmount(); - const data = "ABCDEFG" - .split("") - .map((letter, index) => ({ letter, position: index + 1 })); + await wait(0); - const link = new ApolloLink((operation) => { - const { offset = 0, limit = 2 } = operation.variables; - const letters = data.slice(offset, offset + limit); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); - return new Observable((observer) => { - setTimeout(() => { - observer.next({ data: { letters } }); - observer.complete(); - }, 10); - }); +it("auto disposes of the queryRef if not used within timeout", async () => { + jest.useFakeTimers(); + const { query } = setupSimpleCase(); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ link, cache: new InMemoryCache() }); + + const { result } = renderHook(() => useBackgroundQuery(query, { client })); + + const [queryRef] = result.current; + + expect(queryRef).not.toBeDisposed(); + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + await act(() => { + link.simulateResult({ result: { data: { greeting: "Hello" } } }, true); + // Ensure simulateResult will deliver the result since its wrapped with + // setTimeout + jest.advanceTimersByTime(10); }); - const cacheWithTypePolicies = new InMemoryCache({ - typePolicies: { - Query: { - fields: { - letters: concatPagination(), + jest.advanceTimersByTime(30_000); + + expect(queryRef).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +it("auto disposes of the queryRef if not used within configured timeout", async () => { + jest.useFakeTimers(); + const { query } = setupSimpleCase(); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + defaultOptions: { + react: { + suspense: { + autoDisposeTimeoutMs: 5000, }, }, }, }); - const client = new ApolloClient({ - cache: fieldPolicies ? cacheWithTypePolicies : new InMemoryCache(), - link, + + const { result } = renderHook(() => useBackgroundQuery(query, { client })); + + const [queryRef] = result.current; + + expect(queryRef).not.toBeDisposed(); + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + await act(() => { + link.simulateResult({ result: { data: { greeting: "Hello" } } }, true); + // Ensure simulateResult will deliver the result since its wrapped with + // setTimeout + jest.advanceTimersByTime(10); }); - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - } - function SuspenseFallback() { - ProfiledApp.mergeSnapshot(({ suspenseCount }) => ({ - suspenseCount: suspenseCount + 1, - })); - return
loading
; - } - - function Child({ - queryRef, - onFetchMore, - }: { - onFetchMore: (options: FetchMoreQueryOptions) => void; - queryRef: QueryReference; - }) { - const { data, error } = useReadQuery(queryRef); - // count renders in the child component - ProfiledApp.mergeSnapshot(({ count }) => ({ - count: count + 1, - })); - return ( -
- {error ? -
{error.message}
- : null} - -
    - {data.letters.map(({ letter, position }) => ( -
  • - {letter} -
  • - ))} -
-
- ); - } + expect(queryRef).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); - function ParentWithVariables() { - const [queryRef, { fetchMore }] = useBackgroundQuery(query, { - variables: { limit: 2, offset: 0 }, - }); - return ; - } +it("will resubscribe after disposed when mounting useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + defaultOptions: { + react: { + suspense: { + // Set this to something really low to avoid fake timers + autoDisposeTimeoutMs: 20, + }, + }, + }, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); function App() { + useTrackRenders(); + const [show, setShow] = React.useState(false); + const [queryRef] = useBackgroundQuery(query); + return ( - - Error} - onError={(error) => { - ProfiledApp.mergeSnapshot(({ errorCount, errors }) => ({ - errorCount: errorCount + 1, - errors: errors.concat(error), - })); - }} - > - }> - - - - + <> + + }> + {show && } + + ); } - const ProfiledApp = profile({ - Component: App, - snapshotDOM: true, - initialSnapshot: { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - } as Renders, - }); - const { ...rest } = render(); - return { ...rest, ProfiledApp, data, query, client }; -} + renderWithClient(, { client, wrapper: Profiler }); -type RenderSuspenseHookOptions = Omit< - RenderHookOptions, - "wrapper" -> & { - client?: ApolloClient; - link?: ApolloLink; - cache?: ApolloCache; - mocks?: MockedResponse[]; - strictMode?: boolean; -}; - -interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: Result[]; -} + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); -interface SimpleQueryData { - greeting: string; -} + { + const { renderedComponents } = await Profiler.takeRender(); -function renderSuspenseHook( - render: (initialProps: Props) => Result, - options: RenderSuspenseHookOptions = Object.create(null) -) { - function SuspenseFallback() { - renders.suspenseCount++; + expect(renderedComponents).toStrictEqual([App]); + } + + // Wait long enough for auto dispose to kick in + await wait(50); - return
loading
; + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + await act(() => user.click(screen.getByText("Toggle"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + client.writeQuery({ + query, + data: { greeting: "Hello again" }, + }); - const { mocks = [], strictMode, ...renderHookOptions } = options; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const client = - options.client || - new ApolloClient({ - cache: options.cache || new InMemoryCache(), - link: options.link || new MockLink(mocks), + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const view = renderHook( - (props) => { - renders.count++; + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const view = render(props); +it("auto resubscribes when mounting useReadQuery after naturally disposed by useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); - renders.frames.push(view); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - return view; - }, - { - ...renderHookOptions, - wrapper: ({ children }) => { - const Wrapper = strictMode ? StrictMode : Fragment; - - return ( - - }> - Error} - onError={(error) => { - renders.errorCount++; - renders.errors.push(error); - }} - > - {children} - - - - ); - }, - } - ); + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + const [queryRef] = useBackgroundQuery(query); - return { ...view, renders }; -} + return ( + <> + + }> + {show && } + + + ); + } -describe("useBackgroundQuery", () => { - it("fetches a simple query with minimal config", async () => { - const query = gql` - query { - hello - } - `; - const mocks = [ - { - request: { query }, - result: { data: { hello: "world 1" } }, - }, - ]; - const { result } = renderHook(() => useBackgroundQuery(query), { - wrapper: ({ children }) => ( - {children} - ), - }); + renderWithClient(, { client, wrapper: Profiler }); - const [queryRef] = result.current; + const toggleButton = screen.getByText("Toggle"); - const _result = await unwrapQueryRef(queryRef).promise; + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); - expect(_result).toEqual({ - data: { hello: "world 1" }, - loading: false, - networkStatus: 7, - }); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - it("allows the client to be overridden", async () => { - const query: TypedDocumentNode = gql` - query UserQuery { - greeting - } - `; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const globalClient = new ApolloClient({ - link: new ApolloLink(() => - Observable.of({ data: { greeting: "global hello" } }) - ), - cache: new InMemoryCache(), - }); + { + const { snapshot } = await Profiler.takeRender(); - const localClient = new ApolloClient({ - link: new ApolloLink(() => - Observable.of({ data: { greeting: "local hello" } }) - ), - cache: new InMemoryCache(), + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const { result } = renderSuspenseHook( - () => useBackgroundQuery(query, { client: localClient }), - { client: globalClient } - ); + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); - const [queryRef] = result.current; + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); - const _result = await unwrapQueryRef(queryRef).promise; + await act(() => user.click(toggleButton)); - await waitFor(() => { - expect(_result).toEqual({ - data: { greeting: "local hello" }, - loading: false, - networkStatus: NetworkStatus.ready, - }); + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - }); + } - it("passes context to the link", async () => { - const query = gql` - query ContextQuery { - context - } - `; + client.writeQuery({ + query, + data: { greeting: "Hello again" }, + }); - const link = new ApolloLink((operation) => { - return new Observable((observer) => { - const { valueA, valueB } = operation.getContext(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - observer.next({ data: { context: { valueA, valueB } } }); - observer.complete(); - }); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const { result } = renderHook( - () => - useBackgroundQuery(query, { - context: { valueA: "A", valueB: "B" }, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const [queryRef] = result.current; +it("allows the client to be overridden", async () => { + const { query } = setupSimpleCase(); - const _result = await unwrapQueryRef(queryRef).promise; + const globalClient = new ApolloClient({ + link: new ApolloLink(() => + Observable.of({ data: { greeting: "global hello" } }) + ), + cache: new InMemoryCache(), + }); - await waitFor(() => { - expect(_result).toMatchObject({ - data: { context: { valueA: "A", valueB: "B" } }, - networkStatus: NetworkStatus.ready, - }); - }); + const localClient = new ApolloClient({ + link: new ApolloLink(() => + Observable.of({ data: { greeting: "local hello" } }) + ), + cache: new InMemoryCache(), }); - it('enables canonical results when canonizeResults is "true"', async () => { - interface Result { - __typename: string; - value: number; - } + const Profiler = createDefaultProfiler(); - const cache = new InMemoryCache({ - typePolicies: { - Result: { - keyFields: false, - }, - }, - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const query: TypedDocumentNode<{ results: Result[] }> = gql` - query { - results { - value - } - } - `; + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { client: localClient }); - 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 }, - ]; + return ( + }> + + + ); + } - cache.writeQuery({ - query, - data: { results }, - }); + renderWithClient(, { client: globalClient, wrapper: Profiler }); - const { result } = renderHook( - () => - useBackgroundQuery(query, { - canonizeResults: true, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + { + const { renderedComponents } = await Profiler.takeRender(); - const [queryRef] = result.current; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const _result = await unwrapQueryRef(queryRef).promise; - const resultSet = new Set(_result.data.results); - const values = Array.from(resultSet).map((item) => item.value); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(_result.data).toEqual({ results }); - expect(_result.data.results.length).toBe(6); - expect(resultSet.size).toBe(5); - expect(values).toEqual([0, 1, 2, 3, 5]); - }); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "local hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - it("can disable canonical results when the cache's canonizeResults setting is true", async () => { - interface Result { - __typename: string; - value: number; + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it("passes context to the link", async () => { + const query = gql` + query ContextQuery { + context } + `; - const cache = new InMemoryCache({ - canonizeResults: true, - typePolicies: { - Result: { - keyFields: false, - }, - }, + const link = new ApolloLink((operation) => { + return new Observable((observer) => { + const { valueA, valueB } = operation.getContext(); + + observer.next({ data: { context: { valueA, valueB } } }); + observer.complete(); }); + }); - const query: TypedDocumentNode<{ results: Result[] }> = gql` - query { - results { - value - } - } - `; + const Profiler = createDefaultProfiler(); - 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 }, - ]; + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - cache.writeQuery({ - query, - data: { results }, + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + context: { valueA: "A", valueB: "B" }, }); - const { result } = renderHook( - () => - useBackgroundQuery(query, { - canonizeResults: false, - }), - { - wrapper: ({ children }) => ( - {children} - ), - } + return ( + }> + + ); + } - const [queryRef] = result.current; + renderWithMocks(, { link, wrapper: Profiler }); - const _result = await unwrapQueryRef(queryRef).promise; - const resultSet = new Set(_result.data.results); - const values = Array.from(resultSet).map((item) => item.value); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(_result.data).toEqual({ results }); - expect(_result.data.results.length).toBe(6); - expect(resultSet.size).toBe(6); - expect(values).toEqual([0, 1, 1, 2, 3, 5]); - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - // TODO(FIXME): test fails, should return cache data first if it exists - it.skip("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { - const query = gql` - { - hello - } - `; - const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link" } }, - delay: 20, - }); + { + const { snapshot } = await Profiler.takeRender(); - const client = new ApolloClient({ - link, - cache, + expect(snapshot.result).toEqual({ + data: { context: { valueA: "A", valueB: "B" } }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } +}); - cache.writeQuery({ query, data: { hello: "from cache" } }); - - const { result } = renderHook( - () => useBackgroundQuery(query, { fetchPolicy: "cache-and-network" }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); - - const [queryRef] = result.current; +it('enables canonical results when canonizeResults is "true"', async () => { + interface Result { + __typename: string; + value: number; + } - const _result = await unwrapQueryRef(queryRef).promise; + interface Data { + results: Result[]; + } - expect(_result).toEqual({ - data: { hello: "from link" }, - loading: false, - networkStatus: 7, - }); + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, }); - it("all data is present in the cache, no network request is made", async () => { - const query = gql` - { - hello + const query: TypedDocumentNode = gql` + query { + results { + value } - `; - const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link" } }, - delay: 20, - }); + } + `; - const client = new ApolloClient({ - link, - cache, - }); + 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: { hello: "from cache" } }); + cache.writeQuery({ query, data: { results } }); - const { result } = renderHook( - () => useBackgroundQuery(query, { fetchPolicy: "cache-first" }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + const Profiler = createDefaultProfiler(); - const [queryRef] = result.current; + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const _result = await unwrapQueryRef(queryRef).promise; + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { canonizeResults: true }); - expect(_result).toEqual({ - data: { hello: "from cache" }, - loading: false, - networkStatus: 7, - }); - }); - it("partial data is present in the cache so it is ignored and network request is made", async () => { - const query = gql` - { - hello - foo - } - `; - const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link", foo: "bar" } }, - delay: 20, - }); + return ( + }> + + + ); + } - const client = new ApolloClient({ - link, - cache, - }); + renderWithMocks(, { cache, wrapper: Profiler }); - // we expect a "Missing field 'foo' while writing result..." error - // when writing hello to the cache, so we'll silence the console.error - const originalConsoleError = console.error; - console.error = () => { - /* noop */ - }; - cache.writeQuery({ query, data: { hello: "from cache" } }); - console.error = originalConsoleError; + const { + snapshot: { result }, + } = await Profiler.takeRender(); - const { result } = renderHook( - () => useBackgroundQuery(query, { fetchPolicy: "cache-first" }), - { - wrapper: ({ children }) => ( - {children} - ), - } - ); + const resultSet = new Set(result!.data.results); + const values = Array.from(resultSet).map((item) => item.value); - const [queryRef] = result.current; + expect(result!.data).toEqual({ results }); + expect(result!.data.results.length).toBe(6); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); +}); - const _result = await unwrapQueryRef(queryRef).promise; +it("can disable canonical results when the cache's canonizeResults setting is true", async () => { + interface Result { + __typename: string; + value: number; + } - expect(_result).toEqual({ - data: { foo: "bar", hello: "from link" }, - loading: false, - networkStatus: 7, - }); + interface Data { + results: Result[]; + } + + const cache = new InMemoryCache({ + canonizeResults: true, + typePolicies: { + Result: { + keyFields: false, + }, + }, }); - it("existing data in the cache is ignored", async () => { - const query = gql` - { - hello + const query: TypedDocumentNode = gql` + query { + results { + value } - `; - const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link" } }, - delay: 20, - }); + } + `; - const client = new ApolloClient({ - link, - cache, - }); + 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: { hello: "from cache" } }); + cache.writeQuery({ query, data: { results } }); - const { result } = renderHook( - () => useBackgroundQuery(query, { fetchPolicy: "network-only" }), - { - wrapper: ({ children }) => ( - {children} - ), - } + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { canonizeResults: false }); + + return ( + }> + + ); + } - const [queryRef] = result.current; + renderWithMocks(, { cache, wrapper: Profiler }); - const _result = await unwrapQueryRef(queryRef).promise; + const { snapshot } = await Profiler.takeRender(); + const result = snapshot.result!; - expect(_result).toEqual({ - data: { hello: "from link" }, - loading: false, - networkStatus: 7, - }); - expect(client.cache.extract()).toEqual({ - ROOT_QUERY: { __typename: "Query", hello: "from link" }, - }); + const resultSet = new Set(result.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(result.data).toEqual({ results }); + expect(result.data.results.length).toBe(6); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); +}); + +it("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { + const { query } = setupSimpleCase(); + const cache = new InMemoryCache(); + const link = mockSingleLink({ + request: { query }, + result: { data: { greeting: "from link" } }, + delay: 20, }); - it("fetches data from the network but does not update the cache", async () => { - const query = gql` - { - hello - } - `; - const cache = new InMemoryCache(); - const link = mockSingleLink({ - request: { query }, - result: { data: { hello: "from link" } }, - delay: 20, - }); + const client = new ApolloClient({ link, cache }); - const client = new ApolloClient({ - link, - cache, - }); + cache.writeQuery({ query, data: { greeting: "from cache" } }); - cache.writeQuery({ query, data: { hello: "from cache" } }); + const Profiler = createDefaultProfiler(); - const { result } = renderHook( - () => useBackgroundQuery(query, { fetchPolicy: "no-cache" }), - { - wrapper: ({ children }) => ( - {children} - ), - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-and-network", + }); + + return ( + }> + + ); + } - const [queryRef] = result.current; + renderWithClient(, { client, wrapper: Profiler }); - const _result = await unwrapQueryRef(queryRef).promise; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(_result).toEqual({ - data: { hello: "from link" }, - loading: false, - networkStatus: 7, - }); - // ...but not updated in the cache - expect(client.cache.extract()).toEqual({ - ROOT_QUERY: { __typename: "Query", hello: "from cache" }, + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "from cache" }, + error: undefined, + networkStatus: NetworkStatus.loading, }); - }); + } - describe("integration tests with useReadQuery", () => { - it("suspends and renders hello", async () => { - const { renders } = renderIntegrationTest(); - // ensure the hook suspends immediately - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("hello")).toBeInTheDocument(); - expect(renders.count).toBe(1); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - it("works with startTransition to change variables", async () => { - type Variables = { - id: string; - }; + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); +it("all data is present in the cache, no network request is made", async () => { + const { query } = setupSimpleCase(); + const cache = new InMemoryCache(); - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } - } - `; + let fetchCount = 0; + const link = new ApolloLink((operation) => { + fetchCount++; + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { greeting: "from link" } }); + observer.complete(); + }, 20); + }); + }); - const mocks: MockedResponse[] = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: false } }, - }, - delay: 10, - }, - { - request: { query, variables: { id: "2" } }, - result: { - data: { - todo: { id: "2", name: "Take out trash", completed: true }, - }, - }, - delay: 10, - }, - ]; + const client = new ApolloClient({ link, cache }); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + cache.writeQuery({ query, data: { greeting: "from cache" } }); - function App() { - return ( - - }> - - - - ); - } + const Profiler = createDefaultProfiler(); - function SuspenseFallback() { - return

Loading

; - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function Parent() { - const [id, setId] = React.useState("1"); - const [queryRef] = useBackgroundQuery(query, { - variables: { id }, - }); - return ; - } + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", + }); - function Todo({ - queryRef, - onChange, - }: { - queryRef: QueryReference; - onChange: (id: string) => void; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todo } = data; - - return ( - <> - -
- {todo.name} - {todo.completed && " (completed)"} -
- - ); - } + return ( + }> + + + ); + } - render(); + renderWithClient(, { client, wrapper: Profiler }); - expect(screen.getByText("Loading")).toBeInTheDocument(); + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(await screen.findByTestId("todo")).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "from cache" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); - const todo = screen.getByTestId("todo"); - const button = screen.getByText("Refresh"); + expect(fetchCount).toBe(0); - expect(todo).toHaveTextContent("Clean room"); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - await act(() => user.click(button)); +it("partial data is present in the cache so it is ignored and network request is made", async () => { + const query = gql` + { + hello + foo + } + `; + const cache = new InMemoryCache(); + const link = mockSingleLink({ + request: { query }, + result: { data: { hello: "from link", foo: "bar" } }, + delay: 20, + }); - // startTransition will avoid rendering the suspense fallback for already - // revealed content if the state update inside the transition causes the - // component to suspend. - // - // Here we should not see the suspense fallback while the component suspends - // until the todo is finished loading. Seeing the suspense fallback is an - // indication that we are suspending the component too late in the process. - expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + const client = new ApolloClient({ link, cache }); + + { + // we expect a "Missing field 'foo' while writing result..." error + // when writing hello to the cache, so we'll silence the console.error + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ query, data: { hello: "from cache" } }); + } - // We can ensure this works with isPending from useTransition in the process - expect(todo).toHaveAttribute("aria-busy", "true"); + const Profiler = createDefaultProfiler(); - // Ensure we are showing the stale UI until the new todo has loaded - expect(todo).toHaveTextContent("Clean room"); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - // Eventually we should see the updated todo content once its done - // suspending. - await waitFor(() => { - expect(todo).toHaveTextContent("Take out trash (completed)"); - }); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", }); - it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { - interface Data { - greeting: { - __typename: string; - message: string; - recipient: { name: string; __typename: string }; - }; - } + return ( + }> + + + ); + } - const query: TypedDocumentNode = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; + renderWithClient(, { client, wrapper: Profiler }); - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - message: "Hello cached", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, - }); - const client = new ApolloClient({ cache, link }); - let renders = 0; - let suspenseCount = 0; - - function App() { - return ( - - }> - - - - ); - } + { + const { renderedComponents } = await Profiler.takeRender(); - function SuspenseFallback() { - suspenseCount++; - return

Loading

; - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - function Parent() { - const [queryRef] = useBackgroundQuery(query, { - fetchPolicy: "cache-and-network", - }); - return ; - } + { + const { snapshot } = await Profiler.takeRender(); - function Todo({ queryRef }: { queryRef: QueryReference }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - const { greeting } = data; - renders++; - - return ( - <> -
Message: {greeting.message}
-
Recipient: {greeting.recipient.name}
-
Network status: {networkStatus}
-
Error: {error ? error.message : "none"}
- - ); - } + expect(snapshot.result).toEqual({ + data: { foo: "bar", hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - render(); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello cached" - ); - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Cached Alice" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 1" // loading - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); +it("existing data in the cache is ignored when fetchPolicy is 'network-only'", async () => { + const { query } = setupSimpleCase(); + const cache = new InMemoryCache(); + const link = mockSingleLink({ + request: { query }, + result: { data: { greeting: "from link" } }, + delay: 20, + }); - link.simulateResult({ - result: { - data: { - greeting: { __typename: "Greeting", message: "Hello world" }, - }, - hasNext: true, - }, - }); + const client = new ApolloClient({ link, cache }); - await waitFor(() => { - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello world" - ); - }); - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Cached Alice" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 7" // ready - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + cache.writeQuery({ query, data: { greeting: "from cache" } }); - link.simulateResult({ - result: { - incremental: [ - { - data: { - recipient: { name: "Alice", __typename: "Person" }, - __typename: "Greeting", - }, - path: ["greeting"], - }, - ], - hasNext: false, - }, - }); + const Profiler = createDefaultProfiler(); - await waitFor(() => { - expect(screen.getByText(/Recipient/i)).toHaveTextContent( - "Recipient: Alice" - ); - }); - expect(screen.getByText(/Message/i)).toHaveTextContent( - "Message: Hello world" - ); - expect(screen.getByText(/Network status/i)).toHaveTextContent( - "Network status: 7" // ready - ); - expect(screen.getByText(/Error/i)).toHaveTextContent("none"); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect(renders).toBe(3); - expect(suspenseCount).toBe(0); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "network-only", }); - }); - - it("reacts to cache updates", async () => { - const { renders, client, query } = renderIntegrationTest(); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + return ( + }> + + + ); + } - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("hello")).toBeInTheDocument(); - expect(renders.count).toBe(1); + renderWithClient(, { client, wrapper: Profiler }); - client.writeQuery({ - query, - data: { foo: { bar: "baz" } }, - }); + { + const { renderedComponents } = await Profiler.takeRender(); - // the parent component re-renders when promise fulfilled - expect(await screen.findByText("baz")).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(1); + { + const { snapshot } = await Profiler.takeRender(); - client.writeQuery({ - query, - data: { foo: { bar: "bat" } }, + expect(snapshot.result).toEqual({ + data: { greeting: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - expect(await screen.findByText("bat")).toBeInTheDocument(); - - expect(renders.suspenseCount).toBe(1); + expect(client.cache.extract()).toEqual({ + ROOT_QUERY: { __typename: "Query", greeting: "from link" }, }); - it("reacts to variables updates", async () => { - const { renders, rerender } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); - - rerender({ variables: { id: "2" } }); - - expect(renders.suspenseCount).toBe(2); - expect(screen.getByText("loading")).toBeInTheDocument(); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); +it("fetches data from the network but does not update the cache when fetchPolicy is 'no-cache'", async () => { + const { query } = setupSimpleCase(); + const cache = new InMemoryCache(); + const link = mockSingleLink({ + request: { query }, + result: { data: { greeting: "from link" } }, + delay: 20, }); - it("does not suspend when `skip` is true", async () => { - interface Data { - greeting: string; - } + const client = new ApolloClient({ link, cache }); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + cache.writeQuery({ query, data: { greeting: "from cache" } }); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + const Profiler = createDefaultProfiler(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function SuspenseFallback() { - return
Loading...
; - } + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { fetchPolicy: "no-cache" }); - function Parent() { - const [queryRef] = useBackgroundQuery(query, { skip: true }); + return ( + }> + + + ); + } - return ( - }> - {queryRef && } - - ); - } + renderWithClient(, { client, wrapper: Profiler }); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + { + const { renderedComponents } = await Profiler.takeRender(); - return
{data.greeting}
; - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - function App() { - return ( - - - - ); - } + { + const { snapshot } = await Profiler.takeRender(); - render(); + expect(snapshot.result).toEqual({ + data: { greeting: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); + expect(client.cache.extract()).toEqual({ + ROOT_QUERY: { __typename: "Query", greeting: "from cache" }, }); - it("does not suspend when using `skipToken` in options", async () => { - interface Data { - greeting: string; - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const query: TypedDocumentNode = gql` - query { - greeting +it("works with startTransition to change variables", 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 = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + todo: { id: "2", name: "Take out trash", completed: true }, + }, + }, + delay: 10, + }, + ]; - function SuspenseFallback() { - return
Loading...
; - } + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); - function Parent() { - const [queryRef] = useBackgroundQuery(query, skipToken); + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); - return ( - }> - {queryRef && } - - ); - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + function App() { + useTrackRenders(); + const [id, setId] = React.useState("1"); + const [isPending, startTransition] = React.useTransition(); + const [queryRef] = useBackgroundQuery(query, { + variables: { id }, + }); - return
{data.greeting}
; - } + Profiler.mergeSnapshot({ isPending }); - function App() { - return ( + return ( + <> + - + }> + + - ); - } - - render(); + + ); + } - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); - }); + renderWithClient(, { client, wrapper: Profiler }); - it("suspends when `skip` becomes `false` after it was `true`", async () => { - interface Data { - greeting: string; - } + { + const { renderedComponents } = await Profiler.takeRender(); - const user = userEvent.setup(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + error: undefined, + networkStatus: NetworkStatus.ready, }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), }); + } - function SuspenseFallback() { - return
Loading...
; - } + await act(() => user.click(screen.getByText("Change todo"))); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery(query, { skip }); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - return ( - <> - - }> - {queryRef && } - - - ); - } + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: true, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - return
{data.greeting}
; - } + // Eventually we should see the updated todo content once its done + // suspending. + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "2", name: "Take out trash", completed: true } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } +}); - function App() { - return ( - - - - ); - } +it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } - render(); + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + const client = new ApolloClient({ cache, link }); - await act(() => user.click(screen.getByText("Run query"))); + const Profiler = createDefaultProfiler(); - expect(screen.getByText("Loading...")).toBeInTheDocument(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-and-network", }); - }); - it("suspends when switching away from `skipToken` in options", async () => { - interface Data { - greeting: string; - } + return ( + }> + + + ); + } - const user = userEvent.setup(); + renderWithClient(, { client, wrapper: Profiler }); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + error: undefined, + networkStatus: NetworkStatus.loading, }); + } - function SuspenseFallback() { - return
Loading...
; - } + link.simulateResult({ + result: { + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + hasNext: true, + }, + }); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : undefined - ); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - return ( - <> - - }> - {queryRef && } - - - ); - } + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + link.simulateResult({ + result: { + incremental: [ + { + data: { + recipient: { name: "Alice", __typename: "Person" }, + __typename: "Greeting", + }, + path: ["greeting"], + }, + ], + hasNext: false, + }, + }); - return
{data.greeting}
; - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - function App() { - return ( - - - - ); - } + 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, + }); + } - render(); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); +it("reacts to cache updates", async () => { + const { query, mocks } = setupSimpleCase(); - await act(() => user.click(screen.getByText("Run query"))); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); - expect(screen.getByText("Loading...")).toBeInTheDocument(); + const Profiler = createDefaultProfiler(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); - }); - }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - it("renders skip result, does not suspend, and maintains `data` when `skip` becomes `true` after it was `false`", async () => { - interface Data { - greeting: string; - } + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); - const user = userEvent.setup(); + return ( + }> + + + ); + } - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + renderWithClient(, { client, wrapper: Profiler }); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + { + const { renderedComponents } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
Loading...
; - } + client.writeQuery({ + query, + data: { greeting: "Hello again" }, + }); - function Parent() { - const [skip, setSkip] = React.useState(false); - const [queryRef] = useBackgroundQuery(query, { skip }); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - return ( - <> - - }> - {queryRef && } - - - ); - } + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello again" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + client.writeQuery({ + query, + data: { greeting: "You again?" }, + }); - return
{data.greeting}
; - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - function App() { - return ( - - - - ); - } + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "You again?" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - render(); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - expect(screen.getByText("Loading...")).toBeInTheDocument(); +it("reacts to variables updates", async () => { + const { query, mocks } = setupVariablesCase(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); - }); + const Profiler = createDefaultProfiler(); - await act(() => user.click(screen.getByText("Toggle skip"))); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + function App({ id }: { id: string }) { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { variables: { id } }); + + return ( + }> + + + ); + } + + const { rerender } = renderWithMocks(, { + mocks, + wrapper: Profiler, }); - it("renders skip result, does not suspend, and maintains `data` when switching back to `skipToken`", async () => { - interface Data { - greeting: string; - } + { + const { renderedComponents } = await Profiler.takeRender(); - const user = userEvent.setup(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + { + const { snapshot } = await Profiler.takeRender(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
Loading...
; - } + rerender(); - function Parent() { - const [skip, setSkip] = React.useState(false); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : undefined - ); + { + const { renderedComponents } = await Profiler.takeRender(); - return ( - <> - - }> - {queryRef && } - - - ); - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + { + const { snapshot } = await Profiler.takeRender(); - return
{data.greeting}
; - } + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "2", name: "Black Widow" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function App() { - return ( - - - - ); - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - render(); +it("does not suspend when `skip` is true", async () => { + const { query, mocks } = setupSimpleCase(); - expect(screen.getByText("Loading...")).toBeInTheDocument(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); - }); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { skip: true }); - await act(() => user.click(screen.getByText("Toggle skip"))); + return ( + }> + {queryRef && } + + ); + } - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); - }); + renderWithMocks(, { mocks, wrapper: Profiler }); - it("does not make network requests when `skip` is `true`", async () => { - interface Data { - greeting: string; - } + const { renderedComponents } = await Profiler.takeRender(); - const user = userEvent.setup(); + expect(renderedComponents).toStrictEqual([App]); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; +it("does not suspend when using `skipToken` in options", async () => { + const { query, mocks } = setupSimpleCase(); - let fetchCount = 0; + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const link = new ApolloLink((operation) => { - return new Observable((observer) => { - fetchCount++; + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, skipToken); - const mock = mocks.find(({ request }) => - equal(request.query, operation.query) - ); + return ( + }> + {queryRef && } + + ); + } - if (!mock) { - throw new Error("Could not find mock for operation"); - } + renderWithMocks(, { mocks, wrapper: Profiler }); - observer.next(mock.result); - observer.complete(); - }); - }); + const { renderedComponents } = await Profiler.takeRender(); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + expect(renderedComponents).toStrictEqual([App]); - function SuspenseFallback() { - return
Loading...
; - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery(query, { skip }); +it("suspends when `skip` becomes `false` after it was `true`", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); - return ( - <> - - }> - {queryRef && } - - - ); - } + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [queryRef] = useBackgroundQuery(query, { skip }); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + return ( + <> + + }> + {queryRef && } + + + ); + } - return
{data.greeting}
; - } + renderWithMocks(, { mocks, wrapper: Profiler }); - function App() { - return ( - - - - ); - } + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } - render(); + await act(() => user.click(screen.getByText("Run query"))); - expect(fetchCount).toBe(0); + { + const { renderedComponents } = await Profiler.takeRender(); - // Toggle skip to `false` - await act(() => user.click(screen.getByText("Toggle skip"))); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(fetchCount).toBe(1); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - // Toggle skip to `true` - await act(() => user.click(screen.getByText("Toggle skip"))); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - expect(fetchCount).toBe(1); - }); +it("suspends when switching away from `skipToken` in options", async () => { + const { query, mocks } = setupSimpleCase(); - it("does not make network requests when `skipToken` is used", async () => { - interface Data { - greeting: string; - } + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const user = userEvent.setup(); + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [queryRef] = useBackgroundQuery(query, skip ? skipToken : undefined); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + return ( + <> + + }> + {queryRef && } + + + ); + } - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + renderWithMocks(, { mocks, wrapper: Profiler }); - let fetchCount = 0; + { + const { renderedComponents } = await Profiler.takeRender(); - const link = new ApolloLink((operation) => { - return new Observable((observer) => { - fetchCount++; + expect(renderedComponents).toStrictEqual([App]); + } - const mock = mocks.find(({ request }) => - equal(request.query, operation.query) - ); + await act(() => user.click(screen.getByText("Run query"))); - if (!mock) { - throw new Error("Could not find mock for operation"); - } + { + const { renderedComponents } = await Profiler.takeRender(); - observer.next(mock.result); - observer.complete(); - }); - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), - }); + { + const { snapshot } = await Profiler.takeRender(); - function SuspenseFallback() { - return
Loading...
; - } + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : undefined - ); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - return ( - <> - - }> - {queryRef && } - - - ); - } +it("renders skip result, does not suspend, and maintains `data` when `skip` becomes `true` after it was `false`", async () => { + const { query, mocks } = setupSimpleCase(); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - return
{data.greeting}
; - } + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(false); + const [queryRef] = useBackgroundQuery(query, { skip }); - function App() { - return ( - - - - ); - } + return ( + <> + + }> + {queryRef && } + + + ); + } - render(); + renderWithMocks(, { mocks, wrapper: Profiler }); - expect(fetchCount).toBe(0); + { + const { renderedComponents } = await Profiler.takeRender(); - // Toggle skip to `false` - await act(() => user.click(screen.getByText("Toggle skip"))); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(fetchCount).toBe(1); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - // Toggle skip to `true` - await act(() => user.click(screen.getByText("Toggle skip"))); - - expect(fetchCount).toBe(1); - }); + await act(() => user.click(screen.getByText("Toggle skip"))); - it("`skip` result is referentially stable", async () => { - interface Data { - greeting: string; - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - interface CurrentResult { - current: Data | undefined; - } + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - const user = userEvent.setup(); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const result: CurrentResult = { - current: undefined, - }; +it("renders skip result, does not suspend, and maintains `data` when switching back to `skipToken`", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(false); + const [queryRef] = useBackgroundQuery(query, skip ? skipToken : undefined); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + return ( + <> + + }> + {queryRef && } + + + ); + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + renderWithMocks(, { mocks, wrapper: Profiler }); - function SuspenseFallback() { - return
Loading...
; - } + { + const { renderedComponents } = await Profiler.takeRender(); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery(query, { skip }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - return ( - <> - - }> - {queryRef && } - - - ); - } + { + const { snapshot } = await Profiler.takeRender(); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - result.current = data; + await act(() => user.click(screen.getByText("Toggle skip"))); - return
{data.greeting}
; - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - function App() { - return ( - - - - ); - } + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - const { rerender } = render(); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const skipResult = result.current; +it("does not make network requests when `skip` is `true`", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); - rerender(); + let fetchCount = 0; - expect(result.current).toBe(skipResult); + const link = new ApolloLink((operation) => { + return new Observable((observer) => { + fetchCount++; - // Toggle skip to `false` - await act(() => user.click(screen.getByText("Toggle skip"))); + const mock = mocks.find(({ request }) => + equal(request.query, operation.query) + ); - expect(screen.getByText("Loading...")).toBeInTheDocument(); + if (!mock) { + throw new Error("Could not find mock for operation"); + } - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + observer.next((mock as any).result); + observer.complete(); }); + }); - const fetchedResult = result.current; + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - rerender(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect(result.current).toBe(fetchedResult); - }); + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [queryRef] = useBackgroundQuery(query, { skip }); - it("`skip` result is referentially stable when using `skipToken`", async () => { - interface Data { - greeting: string; - } + return ( + <> + + }> + {queryRef && } + + + ); + } - interface CurrentResult { - current: Data | undefined; - } + renderWithClient(, { client, wrapper: Profiler }); - const user = userEvent.setup(); + // initial skipped result + await Profiler.takeRender(); + expect(fetchCount).toBe(0); - const result: CurrentResult = { - current: undefined, - }; + // Toggle skip to `false` + await act(() => user.click(screen.getByText("Toggle skip"))); - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + expect(fetchCount).toBe(1); + { + const { renderedComponents } = await Profiler.takeRender(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - ]; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
Loading...
; - } + // Toggle skip to `true` + await act(() => user.click(screen.getByText("Toggle skip"))); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : undefined - ); + expect(fetchCount).toBe(1); + { + const { snapshot } = await Profiler.takeRender(); - return ( - <> - - }> - {queryRef && } - - - ); - } + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); +it("does not make network requests when `skipToken` is used", async () => { + const { query, mocks } = setupSimpleCase(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const user = userEvent.setup(); - result.current = data; + let fetchCount = 0; - return
{data.greeting}
; - } + const link = new ApolloLink((operation) => { + return new Observable((observer) => { + fetchCount++; - function App() { - return ( - - - + const mock = mocks.find(({ request }) => + equal(request.query, operation.query) ); - } - const { rerender } = render(); + if (!mock) { + throw new Error("Could not find mock for operation"); + } - const skipResult = result.current; + observer.next((mock as any).result); + observer.complete(); + }); + }); - rerender(); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); - expect(result.current).toBe(skipResult); + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [queryRef] = useBackgroundQuery(query, skip ? skipToken : undefined); - // Toggle skip to `false` - await act(() => user.click(screen.getByText("Toggle skip"))); + return ( + <> + + }> + {queryRef && } + + + ); + } - expect(screen.getByText("Loading...")).toBeInTheDocument(); + renderWithClient(, { client, wrapper: Profiler }); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); - }); + // initial skipped result + await Profiler.takeRender(); + expect(fetchCount).toBe(0); - const fetchedResult = result.current; + // Toggle skip to `false` + await act(() => user.click(screen.getByText("Toggle skip"))); - rerender(); + expect(fetchCount).toBe(1); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(result.current).toBe(fetchedResult); - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - it("`skip` option works with `startTransition`", async () => { - interface Data { - greeting: string; - } + { + const { snapshot } = await Profiler.takeRender(); - const user = userEvent.setup(); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + // Toggle skip to `true` + await act(() => user.click(screen.getByText("Toggle skip"))); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - delay: 10, - }, - ]; + expect(fetchCount).toBe(1); + { + const { snapshot } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } +}); - function SuspenseFallback() { - return
Loading...
; - } - - function Parent() { - const [skip, setSkip] = React.useState(true); - const [isPending, startTransition] = React.useTransition(); - const [queryRef] = useBackgroundQuery(query, { skip }); +it("result is referentially stable", async () => { + const { query, mocks } = setupSimpleCase(); - return ( - <> - - }> - {queryRef && } - - - ); - } + let result: UseReadQueryResult | null = null; - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - return
{data.greeting}
; - } + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query); - function App() { - return ( - - - - ); - } + return ( + }> + + + ); + } - render(); + const { rerender } = renderWithMocks(, { mocks, wrapper: Profiler }); - const button = screen.getByText("Toggle skip"); + { + const { renderedComponents } = await Profiler.takeRender(); - // Toggle skip to `false` - await act(() => user.click(button)); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(button).toBeDisabled(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - }); - - it("`skipToken` works with `startTransition`", async () => { - interface Data { - greeting: string; - } - const user = userEvent.setup(); + result = snapshot.result; + } - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + rerender(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - delay: 10, - }, - ]; + { + const { snapshot } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + expect(snapshot.result).toBe(result); + } +}); - function SuspenseFallback() { - return
Loading...
; - } +it("`skip` option works with `startTransition`", async () => { + const { query, mocks } = setupSimpleCase(); - function Parent() { - const [skip, setSkip] = React.useState(true); - const [isPending, startTransition] = React.useTransition(); - const [queryRef] = useBackgroundQuery( - query, - skip ? skipToken : undefined - ); + const user = userEvent.setup(); + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - return ( - <> - - }> - {queryRef && } - - - ); - } + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [isPending, startTransition] = React.useTransition(); + const [queryRef] = useBackgroundQuery(query, { skip }); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + Profiler.mergeSnapshot({ isPending }); - return
{data.greeting}
; - } + return ( + <> + + }> + {queryRef && } + + + ); + } - function App() { - return ( - - - - ); - } + renderWithMocks(, { mocks, wrapper: Profiler }); - render(); + { + const { renderedComponents } = await Profiler.takeRender(); - const button = screen.getByText("Toggle skip"); + expect(renderedComponents).toStrictEqual([App]); + } - // Toggle skip to `false` - await act(() => user.click(button)); + // Toggle skip to `false` + await act(() => user.click(screen.getByText("Toggle skip"))); - expect(screen.queryByText("Loading...")).not.toBeInTheDocument(); - expect(button).toBeDisabled(); - expect(screen.queryByTestId("greeting")).not.toBeInTheDocument(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("greeting")).toHaveTextContent("Hello"); + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot).toEqual({ + isPending: true, + result: null, }); - }); - - it("applies `errorPolicy` on next fetch when it changes between renders", async () => { - interface Data { - greeting: string; - } - - const user = userEvent.setup(); + } - const query: TypedDocumentNode = gql` - query { - greeting - } - `; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, - }, - { - request: { query }, - result: { - errors: [new GraphQLError("oops")], - }, + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), }); + } - function SuspenseFallback() { - return
Loading...
; - } - - function Parent() { - const [errorPolicy, setErrorPolicy] = React.useState("none"); - const [queryRef, { refetch }] = useBackgroundQuery(query, { - errorPolicy, - }); - - return ( - <> - - - }> - - - - ); - } + await expect(Profiler).not.toRerender(); +}); - function Greeting({ queryRef }: { queryRef: QueryReference }) { - const { data, error } = useReadQuery(queryRef); +it("`skipToken` works with `startTransition`", async () => { + const { query, mocks } = setupSimpleCase(); + const user = userEvent.setup(); - return error ? -
{error.message}
- :
{data.greeting}
; - } + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); - function App() { - return ( - - Error boundary} - > - - - - ); - } + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - render(); + function App() { + useTrackRenders(); + const [skip, setSkip] = React.useState(true); + const [isPending, startTransition] = React.useTransition(); + const [queryRef] = useBackgroundQuery(query, skip ? skipToken : undefined); - expect(await screen.findByTestId("greeting")).toHaveTextContent("Hello"); + Profiler.mergeSnapshot({ isPending }); - await act(() => user.click(screen.getByText("Change error policy"))); - await act(() => user.click(screen.getByText("Refetch greeting"))); + return ( + <> + + }> + {queryRef && } + + + ); + } - // Ensure we aren't rendering the error boundary and instead rendering the - // error message in the Greeting component. - expect(await screen.findByTestId("error")).toHaveTextContent("oops"); - }); + renderWithMocks(, { mocks, wrapper: Profiler }); - it("applies `context` on next fetch when it changes between renders", async () => { - interface Data { - context: Record; - } + { + const { renderedComponents } = await Profiler.takeRender(); - const user = userEvent.setup(); + expect(renderedComponents).toStrictEqual([App]); + } - const query: TypedDocumentNode = gql` - query { - context - } - `; + // Toggle skip to `false` + await act(() => user.click(screen.getByText("Toggle skip"))); - const link = new ApolloLink((operation) => { - return Observable.of({ - data: { - context: operation.getContext(), - }, - }); - }); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const client = new ApolloClient({ - link, - cache: new InMemoryCache(), + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot).toEqual({ + isPending: true, + result: null, }); + } - function SuspenseFallback() { - return
Loading...
; - } - - function Parent() { - const [phase, setPhase] = React.useState("initial"); - const [queryRef, { refetch }] = useBackgroundQuery(query, { - context: { phase }, - }); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - return ( - <> - - - }> - - - - ); - } + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } - function Context({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - return
{data.context.phase}
; - } +it("applies `errorPolicy` on next fetch when it changes between renders", async () => { + const { query } = setupSimpleCase(); + const user = userEvent.setup(); - function App() { - return ( - - - - ); - } + const mocks = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + delay: 10, + }, + { + request: { query }, + result: { + errors: [new GraphQLError("oops")], + }, + delay: 10, + }, + ]; - render(); + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); - expect(await screen.findByTestId("context")).toHaveTextContent("initial"); + function App() { + useTrackRenders(); + const [errorPolicy, setErrorPolicy] = React.useState("none"); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + errorPolicy, + }); - await act(() => user.click(screen.getByText("Update context"))); - await act(() => user.click(screen.getByText("Refetch"))); + return ( + <> + + + }> + + + + + + ); + } - expect(await screen.findByTestId("context")).toHaveTextContent("rerender"); - }); + renderWithMocks(, { mocks, wrapper: Profiler }); - // NOTE: We only test the `false` -> `true` path here. If the option changes - // from `true` -> `false`, the data has already been canonized, so it has no - // effect on the output. - it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { - interface Result { - __typename: string; - value: number; - } + // initial render + await Profiler.takeRender(); - interface Data { - results: Result[]; - } + { + const { snapshot } = await Profiler.takeRender(); - const cache = new InMemoryCache({ - typePolicies: { - Result: { - keyFields: false, - }, + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Change error policy"))); + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Refetch greeting"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + error: null, + result: { + data: { greeting: "Hello" }, + error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), + networkStatus: NetworkStatus.error, }, }); + } +}); - const query: TypedDocumentNode = gql` - query { - results { - value - } - } - `; +it("applies `context` on next fetch when it changes between renders", async () => { + interface Data { + context: Record; + } - 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 }, - ]; + const user = userEvent.setup(); - const user = userEvent.setup(); + const query: TypedDocumentNode = gql` + query { + context + } + `; - cache.writeQuery({ - query, - data: { results }, + const link = new ApolloLink((operation) => { + return new Observable((observer) => { + setTimeout(() => { + const { phase } = operation.getContext(); + observer.next({ data: { context: { phase } } }); + observer.complete(); + }, 10); }); + }); - const client = new ApolloClient({ - link: new MockLink([]), - cache, + const client = new ApolloClient({ link, cache: new InMemoryCache() }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [phase, setPhase] = React.useState("initial"); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + context: { phase }, }); - const result: { current: Data | null } = { - current: null, - }; + return ( + <> + + + }> + + + + ); + } - function SuspenseFallback() { - return
Loading...
; - } + renderWithClient(, { client, wrapper: Profiler }); - function Parent() { - const [canonizeResults, setCanonizeResults] = React.useState(false); - const [queryRef] = useBackgroundQuery(query, { - canonizeResults, - }); + { + const { renderedComponents } = await Profiler.takeRender(); - return ( - <> - - }> - - - - ); - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - function Results({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + { + const { snapshot } = await Profiler.takeRender(); - result.current = data; + expect(snapshot.result).toEqual({ + data: { context: { phase: "initial" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - return null; - } + await act(() => user.click(screen.getByText("Update context"))); + await Profiler.takeRender(); - function App() { - return ( - - - - ); - } + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { context: { phase: "rerender" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); - render(); +// NOTE: We only test the `false` -> `true` path here. If the option changes +// from `true` -> `false`, the data has already been canonized, so it has no +// effect on the output. +it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { + interface Result { + __typename: string; + value: number; + } - function verifyCanonicalResults(data: Data, canonized: boolean) { - const resultSet = new Set(data.results); - const values = Array.from(resultSet).map((item) => item.value); + interface Data { + results: Result[]; + } - expect(data).toEqual({ results }); + const cache = new InMemoryCache({ + typePolicies: { + Result: { + keyFields: false, + }, + }, + }); - if (canonized) { - expect(data.results.length).toBe(6); - expect(resultSet.size).toBe(5); - expect(values).toEqual([0, 1, 2, 3, 5]); - } else { - expect(data.results.length).toBe(6); - expect(resultSet.size).toBe(6); - expect(values).toEqual([0, 1, 1, 2, 3, 5]); + const query: TypedDocumentNode = gql` + query { + results { + value } } + `; - verifyCanonicalResults(result.current!, false); + 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 }, + ]; - await act(() => user.click(screen.getByText("Canonize results"))); + const user = userEvent.setup(); - verifyCanonicalResults(result.current!, true); + cache.writeQuery({ + query, + data: { results }, }); - it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { - interface Data { - primes: number[]; - } + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const user = userEvent.setup(); + function App() { + useTrackRenders(); + const [canonizeResults, setCanonizeResults] = React.useState(false); + const [queryRef] = useBackgroundQuery(query, { + canonizeResults, + }); - const query: TypedDocumentNode = gql` - query GetPrimes($min: number, $max: number) { - primes(min: $min, max: $max) - } - `; + return ( + <> + + }> + + + + ); + } - const mocks = [ - { - request: { query, variables: { min: 0, max: 12 } }, - result: { data: { primes: [2, 3, 5, 7, 11] } }, - }, - { - request: { query, variables: { min: 12, max: 30 } }, - result: { data: { primes: [13, 17, 19, 23, 29] } }, - delay: 10, - }, - { - request: { query, variables: { min: 30, max: 50 } }, - result: { data: { primes: [31, 37, 41, 43, 47] } }, - delay: 10, - }, - ]; + renderWithMocks(, { cache, wrapper: Profiler }); - const mergeParams: [number[] | undefined, number[]][] = []; + { + const { snapshot } = await Profiler.takeRender(); + const result = snapshot.result!; + const resultSet = new Set(result.data.results); + const values = Array.from(resultSet).map((item) => item.value); - 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; - }, + expect(result.data).toEqual({ results }); + expect(result.data.results.length).toBe(6); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); + } + + await act(() => user.click(screen.getByText("Canonize results"))); + + { + const { snapshot } = await Profiler.takeRender(); + const result = snapshot.result!; + const resultSet = new Set(result.data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(result.data).toEqual({ results }); + expect(result.data.results.length).toBe(6); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); + } +}); + +it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { + interface Data { + primes: number[]; + } + + const user = userEvent.setup(); + + const query: TypedDocumentNode = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + 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, + }, + { + request: { query, variables: { min: 30, max: 50 } }, + result: { data: { primes: [31, 37, 41, 43, 47] } }, + 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 client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function SuspenseFallback() { - return
Loading...
; - } + }, + }); - function Parent() { - const [refetchWritePolicy, setRefetchWritePolicy] = - React.useState("merge"); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - const [queryRef, { refetch }] = useBackgroundQuery(query, { - refetchWritePolicy, - variables: { min: 0, max: 12 }, - }); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - return ( - <> - - - - }> - - - - ); - } + function App() { + useTrackRenders(); + const [refetchWritePolicy, setRefetchWritePolicy] = + React.useState("merge"); - function Primes({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + refetchWritePolicy, + variables: { min: 0, max: 12 }, + }); - return {data.primes.join(", ")}; - } + return ( + <> + + + + }> + + + + ); + } - function App() { - return ( - - - - ); - } + renderWithClient(, { client, wrapper: Profiler }); - render(); + // initial suspended render + await Profiler.takeRender(); - const primes = await screen.findByTestId("primes"); + { + const { snapshot } = await Profiler.takeRender(); - expect(primes).toHaveTextContent("2, 3, 5, 7, 11"); + 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 next"))); + await act(() => user.click(screen.getByText("Refetch next"))); + await Profiler.takeRender(); - await waitFor(() => { - expect(primes).toHaveTextContent("2, 3, 5, 7, 11, 13, 17, 19, 23, 29"); - }); + { + 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]], [ @@ -2749,17 +2433,22 @@ describe("useBackgroundQuery", () => { [13, 17, 19, 23, 29], ], ]); + } - await act(() => - user.click(screen.getByText("Change refetch write policy")) - ); + await act(() => user.click(screen.getByText("Change refetch write policy"))); + await Profiler.takeRender(); - await act(() => user.click(screen.getByText("Refetch last"))); + await act(() => user.click(screen.getByText("Refetch last"))); + await Profiler.takeRender(); - await waitFor(() => { - expect(primes).toHaveTextContent("31, 37, 41, 43, 47"); - }); + { + const { snapshot } = await Profiler.takeRender(); + expect(snapshot.result).toEqual({ + data: { primes: [31, 37, 41, 43, 47] }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); expect(mergeParams).toEqual([ [undefined, [2, 3, 5, 7, 11]], [ @@ -2768,3012 +2457,3272 @@ describe("useBackgroundQuery", () => { ], [undefined, [31, 37, 41, 43, 47]], ]); - }); + } +}); - it("applies `returnPartialData` on next fetch when it changes between renders", async () => { - interface Data { - character: { - __typename: "Character"; - id: string; - name: string; - }; - } +it("applies `returnPartialData` on next fetch when it changes between renders", async () => { + const { query } = setupVariablesCase(); - interface PartialData { - character: { - __typename: "Character"; - id: string; - }; - } + interface PartialData { + character: { + __typename: "Character"; + id: string; + }; + } - const user = userEvent.setup(); + const user = userEvent.setup(); - const fullQuery: TypedDocumentNode = gql` - query { - character { - __typename - id - name - } + const partialQuery: TypedDocumentNode = gql` + query { + character { + __typename + id } - `; + } + `; - const partialQuery: TypedDocumentNode = gql` - query { - character { - __typename - id - } - } - `; - - const mocks = [ - { - request: { query: fullQuery }, - result: { - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strange", - }, + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", }, }, }, - { - request: { query: fullQuery }, - result: { - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strange (refetched)", - }, + delay: 10, + }, + { + request: { query }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange (refetched)", }, }, - delay: 100, }, - ]; - - const cache = new InMemoryCache(); - - cache.writeQuery({ - query: partialQuery, - data: { character: { __typename: "Character", id: "1" } }, - }); - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function SuspenseFallback() { - return
Loading...
; - } - - function Parent() { - const [returnPartialData, setReturnPartialData] = React.useState(false); + delay: 10, + }, + ]; - const [queryRef] = useBackgroundQuery(fullQuery, { - returnPartialData, - }); + const cache = new InMemoryCache(); - return ( - <> - - }> - - - - ); - } + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + }); - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - return ( - {data.character.name ?? "unknown"} - ); - } + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function App() { - return ( - - - - ); - } + function App() { + useTrackRenders(); + const [returnPartialData, setReturnPartialData] = React.useState(false); + const [queryRef] = useBackgroundQuery(query, { returnPartialData }); - render(); + return ( + <> + + }> + + + + ); + } - const character = await screen.findByTestId("character"); + renderWithClient(, { client, wrapper: Profiler }); - expect(character).toHaveTextContent("Doctor Strange"); + // initial suspended render + await Profiler.takeRender(); - await act(() => user.click(screen.getByText("Update partial data"))); + { + const { snapshot } = await Profiler.takeRender(); - cache.modify({ - id: cache.identify({ __typename: "Character", id: "1" }), - fields: { - name: (_, { DELETE }) => DELETE, + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Doctor Strange" }, }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - await waitFor(() => { - expect(character).toHaveTextContent("unknown"); - }); + await act(() => user.click(screen.getByText("Update partial data"))); + await Profiler.takeRender(); - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strange (refetched)"); - }); + cache.modify({ + id: cache.identify({ __typename: "Character", id: "1" }), + fields: { + name: (_, { DELETE }) => DELETE, + }, }); - it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { - interface Data { - character: { - __typename: "Character"; - id: string; - name: string; - }; - } - - const user = userEvent.setup(); - - const query: TypedDocumentNode = gql` - query { - character { - __typename - id - name - } - } - `; + { + const { snapshot } = await Profiler.takeRender(); - const mocks = [ - { - request: { query }, - result: { - data: { - character: { - __typename: "Character", - id: "1", - name: "Doctor Strange", - }, - }, - }, - delay: 10, - }, - ]; + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } - const cache = new InMemoryCache(); + { + const { snapshot } = await Profiler.takeRender(); - cache.writeQuery({ - query, + expect(snapshot.result).toEqual({ data: { character: { __typename: "Character", id: "1", - name: "Doctor Strangecache", + name: "Doctor Strange (refetched)", }, }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } +}); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function SuspenseFallback() { - return
Loading...
; - } - - function Parent() { - const [fetchPolicy, setFetchPolicy] = - React.useState("cache-first"); - - const [queryRef, { refetch }] = useBackgroundQuery(query, { - fetchPolicy, - }); - - return ( - <> - - - }> - - - - ); - } - - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data } = useReadQuery(queryRef); +it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { + const { query, mocks } = setupVariablesCase(); - return {data.character.name}; - } + const user = userEvent.setup(); + const cache = new InMemoryCache(); - function App() { - return ( - - - - ); - } + cache.writeQuery({ + query, + variables: { id: "1" }, + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Cacheman", + }, + }, + }); - render(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - const character = await screen.findByTestId("character"); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect(character).toHaveTextContent("Doctor Strangecache"); + function App() { + const [fetchPolicy, setFetchPolicy] = + React.useState("cache-first"); - await act(() => user.click(screen.getByText("Change fetch policy"))); - await act(() => user.click(screen.getByText("Refetch"))); - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strange"); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + fetchPolicy, + variables: { id: "1" }, }); - // Because we switched to a `no-cache` fetch policy, we should not see the - // newly fetched data in the cache after the fetch occurred. - expect(cache.readQuery({ query })).toEqual({ - character: { - __typename: "Character", - id: "1", - name: "Doctor Strangecache", - }, - }); - }); + return ( + <> + + + }> + + + + ); + } - it("properly handles changing options along with changing `variables`", async () => { - interface Data { - character: { - __typename: "Character"; - id: string; - name: string; - }; - } + renderWithClient(, { client, wrapper: Profiler }); - const user = userEvent.setup(); - const query: TypedDocumentNode = gql` - query ($id: ID!) { - character(id: $id) { - __typename - id - name - } - } - `; + { + const { snapshot } = await Profiler.takeRender(); - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("oops")], - }, - delay: 10, - }, - { - request: { query, variables: { id: "2" } }, - result: { - data: { - character: { - __typename: "Character", - id: "2", - name: "Hulk", - }, - }, + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Cacheman", }, - delay: 10, }, - ]; + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - const cache = new InMemoryCache(); + await act(() => user.click(screen.getByText("Change fetch policy"))); + { + const { snapshot } = await Profiler.takeRender(); - cache.writeQuery({ - query, - variables: { - id: "1", - }, + // ensure we haven't changed the result yet just by changing the fetch policy + expect(snapshot.result).toEqual({ data: { character: { __typename: "Character", id: "1", - name: "Doctor Strangecache", + name: "Spider-Cacheman", }, }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); - - function SuspenseFallback() { - return
Loading...
; - } - - function Parent() { - const [id, setId] = React.useState("1"); + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); - const [queryRef, { refetch }] = useBackgroundQuery(query, { - errorPolicy: id === "1" ? "all" : "none", - variables: { id }, - }); + { + const { snapshot } = await Profiler.takeRender(); - return ( - <> - - - - Error boundary} - > - }> - - - - - ); - } + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function Character({ queryRef }: { queryRef: QueryReference }) { - const { data, error } = useReadQuery(queryRef); + // Because we switched to a `no-cache` fetch policy, we should not see the + // newly fetched data in the cache after the fetch occurred. + expect(cache.readQuery({ query, variables: { id: "1" } })).toEqual({ + character: { + __typename: "Character", + id: "1", + name: "Spider-Cacheman", + }, + }); +}); - return error ? -
{error.message}
- : {data.character.name}; - } +it("properly handles changing options along with changing `variables`", async () => { + const { query } = setupVariablesCase(); + const user = userEvent.setup(); + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("oops")], + }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + character: { + __typename: "Character", + id: "2", + name: "Hulk", + }, + }, + }, + delay: 10, + }, + ]; - function App() { - return ( - - - - ); - } + const cache = new InMemoryCache(); - render(); + cache.writeQuery({ + query, + variables: { + id: "1", + }, + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + }); - const character = await screen.findByTestId("character"); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - expect(character).toHaveTextContent("Doctor Strangecache"); + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); - await act(() => user.click(screen.getByText("Get second character"))); + function App() { + useTrackRenders(); + const [id, setId] = React.useState("1"); - await waitFor(() => { - expect(character).toHaveTextContent("Hulk"); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + errorPolicy: id === "1" ? "all" : "none", + variables: { id }, }); - await act(() => user.click(screen.getByText("Get first character"))); + return ( + <> + + + + + }> + + + + + ); + } - await waitFor(() => { - expect(character).toHaveTextContent("Doctor Strangecache"); - }); + renderWithClient(, { client, wrapper: Profiler }); - await act(() => user.click(screen.getByText("Refetch"))); + { + const { snapshot } = await Profiler.takeRender(); - // Ensure we render the inline error instead of the error boundary, which - // tells us the error policy was properly applied. - expect(await screen.findByTestId("error")).toHaveTextContent("oops"); - }); + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } - describe("refetch", () => { - it("re-suspends when calling `refetch`", async () => { - const { ProfiledApp } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); + await act(() => user.click(screen.getByText("Get second character"))); + await Profiler.takeRender(); - { - const { withinDOM, snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(1); - expect(withinDOM().getByText("loading")).toBeInTheDocument(); - } + { + const { snapshot } = await Profiler.takeRender(); - { - const { withinDOM } = await ProfiledApp.takeRender(); - expect(withinDOM().getByText("1 - Spider-Man")).toBeInTheDocument(); - } + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "2", + name: "Hulk", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + await act(() => user.click(screen.getByText("Get first character"))); - { - // parent component re-suspends - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(2); - } - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - // @jerelmiller can you please verify that this is still in the spirit of the test? - // This seems to have moved onto the next render - or before the test skipped one. - expect(snapshot.count).toBe(2); - expect( - withinDOM().getByText("1 - Spider-Man (updated)") - ).toBeInTheDocument(); - } + { + const { snapshot } = await Profiler.takeRender(); - expect(ProfiledApp).not.toRerender(); + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, }); - it("re-suspends when calling `refetch` with new variables", async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + } - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "2" } }, - result: { - data: { character: { id: "2", name: "Captain America" } }, + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + // Ensure we render the inline error instead of the error boundary, which + // tells us the error policy was properly applied. + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", }, }, - ]; - - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - mocks, - }); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); + error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), + networkStatus: NetworkStatus.error, + }, + }); + } +}); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); +it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { + const { query, mocks } = setupVariablesCase(); + const cache = new InMemoryCache(); - const newVariablesRefetchButton = screen.getByText( - "Set variables to id: 2" - ); - const refetchButton = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(newVariablesRefetchButton)); - await act(() => user.click(refetchButton)); + { + // Disable missing field warning + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + // @ts-expect-error writing partial query data + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); + } - expect( - await screen.findByText("2 - Captain America") - ).toBeInTheDocument(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(3); + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - // extra render puts an additional frame into the array - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - ...mocks[1].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + variables: { id: "1" }, }); - it("re-suspends multiple times when calling `refetch` multiple times", async () => { - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - }); - - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); - - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + return ( + }> + + + ); + } - // parent component re-suspends - expect(renders.suspenseCount).toBe(2); - expect(renders.count).toBe(2); + renderWithClient(, { client, wrapper: Profiler }); - expect( - await screen.findByText("1 - Spider-Man (updated)") - ).toBeInTheDocument(); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - await act(() => user.click(button)); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } - // parent component re-suspends - expect(renders.suspenseCount).toBe(3); - expect(renders.count).toBe(3); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect( - await screen.findByText("1 - Spider-Man (updated again)") - ).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - it("throws errors when errors are returned after calling `refetch`", async () => { - using _consoleSpy = spyOnConsole("error"); - interface QueryData { - character: { - id: string; - name: string; - }; - } + } - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], - }, - }, - ]; - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - mocks, - }); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - expect(renders.suspenseCount).toBe(1); - expect(screen.getByText("loading")).toBeInTheDocument(); +it('suspends and does not use partial data from other variables in the cache when changing variables and using a "cache-first" fetch policy with returnPartialData: true', async () => { + const { query, mocks } = setupVariablesCase(); + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id + } + } + `; - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + const cache = new InMemoryCache(); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); - await waitFor(() => { - expect(renders.errorCount).toBe(1); - }); + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect(renders.errors).toEqual([ - new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }), - ]); + function App({ id }: { id: string }) { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + variables: { id }, }); - it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } - - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], - }, - }, - ]; - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "ignore", - mocks, - }); + return ( + }> + + + ); + } - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + const { rerender } = renderWithMocks(, { + cache, + mocks, + wrapper: Profiler, + }); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, }); - it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + } - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - errors: [new GraphQLError("Something went wrong")], - }, - }, - ]; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "all", - mocks, - }); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + rerender(); - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect( - await screen.findByText("Something went wrong") - ).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "2", name: "Black Widow" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { - interface QueryData { - character: { - id: string; - name: string; - }; - } + } - interface QueryVariables { - id: string; - } - const query: TypedDocumentNode = gql` - query CharacterQuery($id: ID!) { - character(id: $id) { - id - name - } - } - `; - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: "Captain Marvel" } }, - }, - }, - { - request: { query, variables: { id: "1" } }, - result: { - data: { character: { id: "1", name: null } }, - errors: [new GraphQLError("Something went wrong")], - }, - }, - ]; + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - const { renders } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - errorPolicy: "all", - mocks, - }); +it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { + const { query, mocks } = setupVariablesCase(); - expect(await screen.findByText("1 - Captain Marvel")).toBeInTheDocument(); + const partialQuery = gql` + query ($id: String!) { + character(id: $id) { + id + } + } + `; - const button = screen.getByText("Refetch"); - const user = userEvent.setup(); - await act(() => user.click(button)); + const cache = new InMemoryCache(); - expect(renders.errorCount).toBe(0); - expect(renders.errors).toEqual([]); + cache.writeQuery({ + query: partialQuery, + variables: { id: "1" }, + data: { character: { __typename: "Character", id: "1" } }, + }); - expect( - await screen.findByText("Something went wrong") - ).toBeInTheDocument(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - const expectedError = new ApolloError({ - graphQLErrors: [new GraphQLError("Something went wrong")], - }); + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - { - data: mocks[1].result.data, - networkStatus: NetworkStatus.error, - error: expectedError, - }, - ]); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "network-only", + returnPartialData: true, + variables: { id: "1" }, }); - it("can refetch after error is encountered", async () => { - type Variables = { - id: string; - }; + return ( + }> + + + ); + } - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); + renderWithClient(, { client, wrapper: Profiler }); - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } - } - `; + { + const { renderedComponents } = await Profiler.takeRender(); - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: null, - errors: [new GraphQLError("Oops couldn't fetch")], - }, - delay: 10, - }, - { - request: { query, variables: { id: "1" } }, - result: { - data: { todo: { id: "1", name: "Clean room", completed: true } }, - }, - delay: 10, - }, - ]; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + { + const { snapshot } = await Profiler.takeRender(); - function App() { - return ( - - - - ); - } + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function SuspenseFallback() { - return

Loading

; - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - function Parent() { - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { id: "1" }, - }); +it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { + using _consoleSpy = spyOnConsole("warn"); + const { query, mocks } = setupVariablesCase(); - return ( - }> - refetch()} - fallbackRender={({ error, resetErrorBoundary }) => ( - <> - -
{error.message}
- - )} - > - -
-
- ); + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id } + } + `; - function Todo({ queryRef }: { queryRef: QueryReference }) { - const { - data: { todo }, - } = useReadQuery(queryRef); - - return ( -
- {todo.name} - {todo.completed && " (completed)"} -
- ); - } + const cache = new InMemoryCache(); - render(); + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); - // Disable error message shown in the console due to an uncaught error. - // TODO: need to determine why the error message is logged to the console - // as an uncaught error since other tests do not require this. - { - using _consoleSpy = spyOnConsole("error"); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - expect(screen.getByText("Loading")).toBeInTheDocument(); + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect( - await screen.findByText("Oops couldn't fetch") - ).toBeInTheDocument(); - } - - const button = screen.getByText("Retry"); - - await act(() => user.click(button)); - - expect(screen.getByText("Loading")).toBeInTheDocument(); - - await waitFor(() => { - expect(screen.getByTestId("todo")).toHaveTextContent( - "Clean room (completed)" - ); - }); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + variables: { id: "1" }, }); - it("throws errors on refetch after error is encountered after first fetch with error", async () => { - type Variables = { - id: string; - }; - - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); + return ( + }> + + + ); + } - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } - } - `; + renderWithClient(, { client, wrapper: Profiler }); - const mocks = [ - { - request: { query, variables: { id: "1" } }, - result: { - data: null, - errors: [new GraphQLError("Oops couldn't fetch")], - }, - delay: 10, - }, - { - request: { query, variables: { id: "1" } }, - result: { - data: null, - errors: [new GraphQLError("Oops couldn't fetch again")], - }, - delay: 10, - }, - ]; + { + const { renderedComponents } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - function App() { - return ( - - - - ); - } + { + const { snapshot } = await Profiler.takeRender(); - function SuspenseFallback() { - return

Loading

; - } + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function Parent() { - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { id: "1" }, - }); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - return ( - }> - refetch()} - fallbackRender={({ error, resetErrorBoundary }) => ( - <> - -
{error.message}
- - )} - > - -
-
- ); - } +it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { + using _consoleSpy = spyOnConsole("warn"); - function Todo({ queryRef }: { queryRef: QueryReference }) { - const { - data: { todo }, - } = useReadQuery(queryRef); - - return ( -
- {todo.name} - {todo.completed && " (completed)"} -
- ); - } + const query: TypedDocumentNode = gql` + query UserQuery { + greeting + } + `; + const mocks = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + }, + ]; - render(); + renderHook( + () => + useBackgroundQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); - // Disable error message shown in the console due to an uncaught error. - // TODO: need to determine why the error message is logged to the console - // as an uncaught error since other tests do not require this. - using _consoleSpy = spyOnConsole("error"); + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy." + ); +}); - expect(screen.getByText("Loading")).toBeInTheDocument(); +it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { + const { query, mocks } = setupVariablesCase(); - expect( - await screen.findByText("Oops couldn't fetch") - ).toBeInTheDocument(); + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id + } + } + `; - const button = screen.getByText("Retry"); + const cache = new InMemoryCache(); - await act(() => user.click(button)); + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); - expect(screen.getByText("Loading")).toBeInTheDocument(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - await waitFor(() => { - expect( - screen.getByText("Oops couldn't fetch again") - ).toBeInTheDocument(); - }); + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-and-network", + returnPartialData: true, + variables: { id: "1" }, }); - it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { - type Variables = { - id: string; - }; - - interface Data { - todo: { - id: string; - name: string; - completed: boolean; - }; - } - const user = userEvent.setup(); + return ( + }> + + + ); + } - const query: TypedDocumentNode = gql` - query TodoItemQuery($id: ID!) { - todo(id: $id) { - id - name - completed - } - } - `; + renderWithClient(, { client, wrapper: Profiler }); - 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 { snapshot, renderedComponents } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache(), - }); + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } - function App() { - return ( - - }> - - - - ); - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - function SuspenseFallback() { - return

Loading

; - } + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function Parent() { - const [id, setId] = React.useState("1"); - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { id }, - }); - const onRefetchHandler = () => { - refetch(); - }; - return ( - - ); - } + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - function Todo({ - queryRef, - onRefetch, - }: { - onRefetch: () => void; - queryRef: QueryReference; - onChange: (id: string) => void; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todo } = data; - - return ( - <> - -
- {todo.name} - {todo.completed && " (completed)"} -
- - ); +it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { + const { query, mocks } = setupVariablesCase(); + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id } + } + `; - const ProfiledApp = profile({ Component: App, snapshotDOM: true }); - - render(); + const cache = new InMemoryCache(); - { - const { withinDOM } = await ProfiledApp.takeRender(); - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - } + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + variables: { id: "1" }, + }); - { - const { withinDOM } = await ProfiledApp.takeRender(); - const todo = withinDOM().getByTestId("todo"); - expect(todo).toBeInTheDocument(); - expect(todo).toHaveTextContent("Clean room"); - } + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const button = screen.getByText("Refresh"); - await act(() => user.click(button)); + function App({ id }: { id: string }) { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-and-network", + returnPartialData: true, + variables: { id }, + }); - // startTransition will avoid rendering the suspense fallback for already - // revealed content if the state update inside the transition causes the - // component to suspend. - // - // Here we should not see the suspense fallback while the component suspends - // until the todo is finished loading. Seeing the suspense fallback is an - // indication that we are suspending the component too late in the process. - { - const { withinDOM } = await ProfiledApp.takeRender(); - const todo = withinDOM().getByTestId("todo"); - expect(withinDOM().queryByText("Loading")).not.toBeInTheDocument(); + return ( + }> + + + ); + } - // We can ensure this works with isPending from useTransition in the process - expect(todo).toHaveAttribute("aria-busy", "true"); + const { rerender } = renderWithMocks(, { + cache, + mocks, + wrapper: Profiler, + }); - // Ensure we are showing the stale UI until the new todo has loaded - expect(todo).toHaveTextContent("Clean room"); - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - // Eventually we should see the updated todo content once its done - // suspending. - { - const { withinDOM } = await ProfiledApp.takeRender(); - const todo = withinDOM().getByTestId("todo"); - expect(todo).toHaveTextContent("Clean room (completed)"); - } + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, }); - }); + } - describe("fetchMore", () => { - function getItemTexts( - screen: Pick = _screen - ) { - return screen.getAllByTestId(/letter/).map( - // eslint-disable-next-line testing-library/no-node-access - (li) => li.firstChild!.textContent - ); - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - it("re-suspends when calling `fetchMore` with different variables", async () => { - const { ProfiledApp } = renderPaginatedIntegrationTest(); + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - { - const { withinDOM, snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(1); - expect(withinDOM().getByText("loading")).toBeInTheDocument(); - } + rerender(); - { - const { withinDOM } = await ProfiledApp.takeRender(); - const items = await screen.findAllByTestId(/letter/i); - expect(items).toHaveLength(2); - expect(getItemTexts(withinDOM())).toStrictEqual(["A", "B"]); - } + { + const { renderedComponents } = await Profiler.takeRender(); - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - { - // parent component re-suspends - const { snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(2); - } - { - // parent component re-suspends - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - expect(snapshot.count).toBe(2); + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(getItemTexts(withinDOM())).toStrictEqual(["C", "D"]); - } + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "2", name: "Black Widow" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - it("properly uses `updateQuery` when calling `fetchMore`", async () => { - const { ProfiledApp } = renderPaginatedIntegrationTest({ - updateQuery: true, - }); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - { - const { withinDOM, snapshot } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(1); - expect(withinDOM().getByText("loading")).toBeInTheDocument(); - } +it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + interface QueryData { + greeting: { + __typename: string; + message?: string; + recipient?: { + __typename: string; + name: string; + }; + }; + } - { - const { withinDOM } = await ProfiledApp.takeRender(); + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; - const items = withinDOM().getAllByTestId(/letter/i); + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); - expect(items).toHaveLength(2); - expect(getItemTexts(withinDOM())).toStrictEqual(["A", "B"]); - } + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + { + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + const client = new ApolloClient({ link, cache }); - { - const { snapshot } = await ProfiledApp.takeRender(); - // parent component re-suspends - expect(snapshot.suspenseCount).toBe(2); - } - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - expect(snapshot.count).toBe(2); + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const moreItems = withinDOM().getAllByTestId(/letter/i); - expect(moreItems).toHaveLength(4); - expect(getItemTexts(withinDOM())).toStrictEqual(["A", "B", "C", "D"]); - } + function App() { + useTrackRenders(); + const [queryRef] = useBackgroundQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, }); - it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { - const { ProfiledApp } = renderPaginatedIntegrationTest({ - fieldPolicies: true, - }); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - expect(snapshot.suspenseCount).toBe(1); - expect(withinDOM().getByText("loading")).toBeInTheDocument(); - } - - { - const { withinDOM } = await ProfiledApp.takeRender(); - const items = withinDOM().getAllByTestId(/letter/i); - expect(items).toHaveLength(2); - expect(getItemTexts(withinDOM())).toStrictEqual(["A", "B"]); - } + return ( + }> + + + ); + } - const button = screen.getByText("Fetch more"); - const user = userEvent.setup(); - await act(() => user.click(button)); + renderWithClient(, { client, wrapper: Profiler }); - { - const { snapshot } = await ProfiledApp.takeRender(); - // parent component re-suspends - expect(snapshot.suspenseCount).toBe(2); - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - { - const { snapshot, withinDOM } = await ProfiledApp.takeRender(); - expect(snapshot.count).toBe(2); - const moreItems = await screen.findAllByTestId(/letter/i); - expect(moreItems).toHaveLength(4); - expect(getItemTexts(withinDOM())).toStrictEqual(["A", "B", "C", "D"]); - } + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, }); - it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { - type Variables = { - offset: number; - }; + } - interface Todo { - __typename: "Todo"; - id: string; - name: string; - completed: boolean; - } - interface Data { - todos: Todo[]; - } - const user = userEvent.setup(); + link.simulateResult({ + result: { + data: { + greeting: { message: "Hello world", __typename: "Greeting" }, + }, + hasNext: true, + }, + }); - const query: TypedDocumentNode = gql` - query TodosQuery($offset: Int!) { - todos(offset: $offset) { - id - name - completed - } - } - `; + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - const mocks: MockedResponse[] = [ - { - request: { query, variables: { offset: 0 } }, - result: { - data: { - todos: [ - { - __typename: "Todo", - id: "1", - name: "Clean room", - completed: false, - }, - ], - }, - }, - delay: 10, + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, }, - { - request: { query, variables: { offset: 1 } }, - result: { + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { data: { - todos: [ - { - __typename: "Todo", - id: "2", - name: "Take out trash", - completed: true, - }, - ], - }, - }, - delay: 10, - }, - ]; - - const client = new ApolloClient({ - link: new MockLink(mocks), - cache: new InMemoryCache({ - typePolicies: { - Query: { - fields: { - todos: offsetLimitPagination(), - }, + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, }, + path: ["greeting"], }, - }), - }); - - function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - return

Loading

; - } - - function Parent() { - const [queryRef, { fetchMore }] = useBackgroundQuery(query, { - variables: { offset: 0 }, - }); - const onFetchMoreHandler = (variables: Variables) => { - fetchMore({ variables }); - }; - return ; - } - - function Todo({ - queryRef, - onFetchMore, - }: { - onFetchMore: (variables: Variables) => void; - queryRef: QueryReference; - }) { - const { data } = useReadQuery(queryRef); - const [isPending, startTransition] = React.useTransition(); - const { todos } = data; - - return ( - <> - -
- {todos.map((todo) => ( -
- {todo.name} - {todo.completed && " (completed)"} -
- ))} -
- - ); - } - - const ProfiledApp = profile({ Component: App, snapshotDOM: true }); - render(); + ], + hasNext: false, + }, + }, + true + ); - { - const { withinDOM } = await ProfiledApp.takeRender(); - expect(withinDOM().getByText("Loading")).toBeInTheDocument(); - } + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - { - const { withinDOM } = await ProfiledApp.takeRender(); - expect(withinDOM().getByTestId("todos")).toBeInTheDocument(); - expect(withinDOM().getByTestId("todo:1")).toBeInTheDocument(); - } + 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, + }); + } - const button = screen.getByText("Load more"); - await act(() => user.click(button)); + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); - { - const { withinDOM } = await ProfiledApp.takeRender(); - // startTransition will avoid rendering the suspense fallback for already - // revealed content if the state update inside the transition causes the - // component to suspend. - // - // Here we should not see the suspense fallback while the component suspends - // until the todo is finished loading. Seeing the suspense fallback is an - // indication that we are suspending the component too late in the process. - expect(withinDOM().queryByText("Loading")).not.toBeInTheDocument(); - - // We can ensure this works with isPending from useTransition in the process - expect(withinDOM().getByTestId("todos")).toHaveAttribute( - "aria-busy", - "true" - ); - - // Ensure we are showing the stale UI until the new todo has loaded - expect(withinDOM().getByTestId("todo:1")).toHaveTextContent( - "Clean room" - ); - } +describe("refetch", () => { + it("re-suspends when calling `refetch`", async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const mocks: MockedResponse[] = [ + ...defaultMocks, { - const { withinDOM } = await ProfiledApp.takeRender(); - // Eventually we should see the updated todos content once its done - // suspending. - expect(withinDOM().getByTestId("todo:2")).toHaveTextContent( - "Take out trash (completed)" - ); - expect(withinDOM().getByTestId("todo:1")).toHaveTextContent( - "Clean room" - ); - } - }); + request: { query, variables: { id: "1" } }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched)", + }, + }, + }, + delay: 10, + }, + ]; - it('honors refetchWritePolicy set to "merge"', async () => { - const user = userEvent.setup(); + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + }); - const query: TypedDocumentNode< - { primes: number[] }, - { min: number; max: number } - > = gql` - query GetPrimes($min: number, $max: number) { - primes(min: $min, max: $max) - } - `; + return ( + <> + + }> + + + + ); + } - interface QueryData { - primes: number[]; - } + renderWithMocks(, { mocks, wrapper: Profiler }); - const mocks = [ - { - request: { query, variables: { min: 0, max: 12 } }, - result: { data: { primes: [2, 3, 5, 7, 11] } }, - }, - { - request: { query, variables: { min: 12, max: 30 } }, - result: { data: { primes: [13, 17, 19, 23, 29] } }, - delay: 10, - }, - ]; + { + const { renderedComponents } = await Profiler.takeRender(); - 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; - }, - }, - }, - }, - }, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - function SuspenseFallback() { - return
loading
; - } + { + const { snapshot } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function Child({ - refetch, - queryRef, - }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); - - return ( -
- -
{data?.primes.join(", ")}
-
{networkStatus}
-
{error?.message || "undefined"}
-
- ); - } + await act(() => user.click(screen.getByText("Refetch"))); - function Parent() { - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { min: 0, max: 12 }, - refetchWritePolicy: "merge", - }); - return ; - } + { + // parent component re-suspends + const { renderedComponents } = await Profiler.takeRender(); - function App() { - return ( - - }> - - - - ); - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - render(); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("primes")).toHaveTextContent( - "2, 3, 5, 7, 11" - ); + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - expect(screen.getByTestId("network-status")).toHaveTextContent( - "7" // ready - ); - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - await act(() => user.click(screen.getByText("Refetch"))); + it("re-suspends when calling `refetch` with new variables", async () => { + const { query, mocks } = setupVariablesCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - await waitFor(() => { - expect(screen.getByTestId("primes")).toHaveTextContent( - "2, 3, 5, 7, 11, 13, 17, 19, 23, 29" - ); + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, }); - expect(screen.getByTestId("network-status")).toHaveTextContent( - "7" // ready + + return ( + <> + + }> + + + ); - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - expect(mergeParams).toEqual([ - [undefined, [2, 3, 5, 7, 11]], - [ - [2, 3, 5, 7, 11], - [13, 17, 19, 23, 29], - ], - ]); - }); + } - it('defaults refetchWritePolicy to "overwrite"', async () => { - const user = userEvent.setup(); + renderWithMocks(, { mocks, wrapper: Profiler }); - const query: TypedDocumentNode< - { primes: number[] }, - { min: number; max: number } - > = gql` - query GetPrimes($min: number, $max: number) { - primes(min: $min, max: $max) - } - `; + { + const { renderedComponents } = await Profiler.takeRender(); - interface QueryData { - primes: number[]; - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const mocks = [ - { - request: { query, variables: { min: 0, max: 12 } }, - result: { data: { primes: [2, 3, 5, 7, 11] } }, - }, - { - request: { query, variables: { min: 12, max: 30 } }, - result: { data: { primes: [13, 17, 19, 23, 29] } }, - delay: 10, - }, - ]; + { + const { snapshot } = await Profiler.takeRender(); - 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; - }, - }, - }, + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", }, }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function SuspenseFallback() { - return
loading
; - } + await act(() => user.click(screen.getByText("Refetch"))); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + { + const { renderedComponents } = await Profiler.takeRender(); - function Child({ - refetch, - queryRef, - }: { - refetch: ( - variables?: Partial | undefined - ) => Promise>; - queryRef: QueryReference; - }) { - const { data, error, networkStatus } = useReadQuery(queryRef); - - return ( -
- -
{data?.primes.join(", ")}
-
{networkStatus}
-
{error?.message || "undefined"}
-
- ); - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - function Parent() { - const [queryRef, { refetch }] = useBackgroundQuery(query, { - variables: { min: 0, max: 12 }, - }); - return ; - } + { + const { snapshot } = await Profiler.takeRender(); - function App() { - return ( - - }> - - - - ); - } + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "2", + name: "Black Widow", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - render(); + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - await waitFor(() => { - expect(screen.getByTestId("primes")).toHaveTextContent( - "2, 3, 5, 7, 11" - ); - }); - expect(screen.getByTestId("network-status")).toHaveTextContent( - "7" // ready - ); - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + it("re-suspends multiple times when calling `refetch` multiple times", async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - await act(() => user.click(screen.getByText("Refetch"))); + const mocks: MockedResponse[] = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched)", + }, + }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched again)", + }, + }, + }, + delay: 10, + }, + ]; - await waitFor(() => { - expect(screen.getByTestId("primes")).toHaveTextContent( - "13, 17, 19, 23, 29" - ); + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, }); - expect(screen.getByTestId("network-status")).toHaveTextContent( - "7" // ready + + return ( + <> + + }> + + + ); - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); - expect(mergeParams).toEqual([ - [undefined, [2, 3, 5, 7, 11]], - [undefined, [13, 17, 19, 23, 29]], - ]); - }); + } - it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { - interface Data { - character: { - id: string; - name: string; - }; - } + renderWithMocks(, { mocks, wrapper: Profiler }); - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; + { + const { renderedComponents } = await Profiler.takeRender(); - const partialQuery = gql` - query { - character { - id - } - } - `; - const mocks = [ - { - request: { query: fullQuery }, - result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, }, - ]; + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - }; + const button = screen.getByText("Refetch"); - const cache = new InMemoryCache(); + await act(() => user.click(button)); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); + { + const { renderedComponents } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - function App() { - return ( - - }> - - - - ); - } + { + const { snapshot } = await Profiler.takeRender(); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function Parent() { - const [queryRef] = useBackgroundQuery(fullQuery, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); - return ; - } + await act(() => user.click(button)); - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.count++; - - return ( - <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } + { + const { renderedComponents } = await Profiler.takeRender(); - render(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(renders.suspenseCount).toBe(0); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - expect(screen.getByTestId("character-name")).toHaveTextContent(""); - expect(screen.getByTestId("network-status")).toHaveTextContent("1"); // loading - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + { + const { snapshot } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("character-name")).toHaveTextContent( - "Doctor Strange" - ); + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (refetched again)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + } - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(0); - }); + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - it('suspends and does not use partial data when changing variables and using a "cache-first" fetch policy with returnPartialData', async () => { - const partialQuery = gql` - query ($id: ID!) { - character(id: $id) { - id - } - } - `; + it("throws errors when errors are returned after calling `refetch`", async () => { + using _consoleSpy = spyOnConsole("error"); + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const mocks: MockedResponse[] = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + delay: 10, + }, + ]; - const cache = new InMemoryCache(); + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { variables: { id: "1" }, }); - const { renders, mocks, rerender } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - cache, - options: { - fetchPolicy: "cache-first", - returnPartialData: true, - }, - }); - expect(renders.suspenseCount).toBe(0); + return ( + <> + + }> + + + + + + ); + } - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + renderWithMocks(, { mocks, wrapper: Profiler }); - rerender({ variables: { id: "2" } }); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(renders.frames[2]).toMatchObject({ - ...mocks[1].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }); + { + const { snapshot } = await Profiler.takeRender(); - expect(renders.count).toBe(3); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toMatchObject([ - { - data: { character: { id: "1" } }, - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - ...mocks[0].result, + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + error: undefined, networkStatus: NetworkStatus.ready, + }, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + 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("Something went wrong")], + }) + ); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); + + it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const mocks = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + delay: 10, + }, + ]; + + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + errorPolicy: "ignore", + }); + + return ( + <> + + }> + + + + + + ); + } + + renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + 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).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); + + it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const mocks = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + delay: 10, + }, + ]; + + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + errorPolicy: "all", + }); + + return ( + <> + + }> + + + + + + ); + } + + renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + 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).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); + + it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { + const { query, mocks: defaultMocks } = setupVariablesCase(); + const user = userEvent.setup(); + const mocks = [ + ...defaultMocks, + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { __typename: "Character", id: "1", name: null } }, + errors: [new GraphQLError("Something went wrong")], + }, + delay: 10, + }, + ]; + + const Profiler = createErrorProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); + + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + errorPolicy: "all", + }); + + return ( + <> + + }> + + + + + + ); + } + + renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man", + }, + }, + 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).toEqual({ + error: null, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: null, + }, + }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); + + it("can refetch after error is encountered", 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 = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: null, + errors: [new GraphQLError("Oops couldn't fetch")], + }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + }, + delay: 10, + }, + ]; + + const Profiler = createErrorProfiler(); + const { SuspenseFallback } = createDefaultTrackedComponents(Profiler); + + function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + useTrackRenders(); + Profiler.mergeSnapshot({ error }); + + return ; + } + + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + }); + + return ( + }> + refetch()} + FallbackComponent={ErrorFallback} + > + + + + ); + } + + function Todo({ queryRef }: { queryRef: QueryReference }) { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + // Disable error message shown in the console due to an uncaught error. + using _consoleSpy = spyOnConsole("error"); + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(snapshot).toEqual({ + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Oops couldn't fetch")], + }), + result: null, + }); + } + + await act(() => user.click(screen.getByText("Retry"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([Todo]); + expect(snapshot).toEqual({ + // TODO: We should reset the snapshot between renders to better capture + // the actual result. This makes it seem like the error is rendered, but + // in this is just leftover from the previous snapshot. + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Oops couldn't fetch")], + }), + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + }); + + it("throws errors on refetch after error is encountered after first fetch with error", async () => { + // Disable error message shown in the console due to an uncaught error. + using _consoleSpy = spyOnConsole("error"); + 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 = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: null, + errors: [new GraphQLError("Oops couldn't fetch")], + }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: null, + errors: [new GraphQLError("Oops couldn't fetch again")], + }, + delay: 10, + }, + ]; + + const Profiler = createErrorProfiler(); + const { SuspenseFallback } = createDefaultTrackedComponents(Profiler); + + function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) { + useTrackRenders(); + Profiler.mergeSnapshot({ error }); + + return ; + } + + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + }); + + return ( + }> + refetch()} + FallbackComponent={ErrorFallback} + > + + + + ); + } + + function Todo({ queryRef }: { queryRef: QueryReference }) { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(snapshot).toEqual({ + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Oops couldn't fetch")], + }), + result: null, + }); + } + + await act(() => user.click(screen.getByText("Retry"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(snapshot).toEqual({ + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Oops couldn't fetch again")], + }), + result: null, + }); + } + }); + + it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", 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 Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { id: "1" }, + }); + + Profiler.mergeSnapshot({ isPending }); + + return ( + <> + + }> + + + + ); + } + + renderWithMocks(, { mocks, 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, }, - { - ...mocks[1].result, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + + { + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component + // suspends until the todo is finished loading. Seeing the suspense + // fallback is an indication that we are suspending the component too late + // in the process. + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + 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(); + + // Eventually we should see the updated todo content once its done + // suspending. + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { + data: { todo: { id: "1", name: "Clean room", completed: true } }, error: undefined, + networkStatus: NetworkStatus.ready, }, - ]); - }); + }); + } + }); - it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { - interface Data { - character: { - id: string; - name: string; - }; - } + it('honors refetchWritePolicy set to "merge"', async () => { + const user = userEvent.setup(); - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } + const query: TypedDocumentNode = + gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) } `; - const partialQuery = gql` - query { - character { - id - } + 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 Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { min: 0, max: 12 }, + refetchWritePolicy: "merge", + }); + + 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: { 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"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + 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], + ], + ]); + } + }); + + it('defaults refetchWritePolicy to "overwrite"', async () => { + const user = userEvent.setup(); + + const query: TypedDocumentNode = + gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) } `; - const mocks = [ - { - request: { query: fullQuery }, - result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + + 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; + }, + }, + }, }, - ]; - - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: DeepPartial; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + }, + }); - const cache = new InMemoryCache(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, + function App() { + useTrackRenders(); + const [queryRef, { refetch }] = useBackgroundQuery(query, { + variables: { min: 0, max: 12 }, }); - function App() { - return ( - - }> - - - - ); - } + return ( + <> + + }> + + + + ); + } - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + renderWithClient(, { client, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); - function Parent() { - const [queryRef] = useBackgroundQuery(fullQuery, { - fetchPolicy: "network-only", - returnPartialData: true, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - return ; - } + { + const { snapshot } = await Profiler.takeRender(); - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.frames.push({ data, networkStatus, error }); - renders.count++; - return ( - <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } + 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]]]); + } - render(); + await act(() => user.click(screen.getByText("Refetch"))); - expect(renders.suspenseCount).toBe(1); + { + const { renderedComponents } = await Profiler.takeRender(); - await waitFor(() => { - expect(screen.getByTestId("character-name")).toHaveTextContent( - "Doctor Strange" - ); - }); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(renders.count).toBe(1); - expect(renders.suspenseCount).toBe(1); + { + const { snapshot } = await Profiler.takeRender(); - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, + 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]], ]); - }); + } + }); +}); - it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { - using _consoleSpy = spyOnConsole("warn"); - interface Data { - character: { - id: string; - name: string; - }; - } +describe("fetchMore", () => { + it("re-suspends when calling `fetchMore` with different variables", async () => { + const { query, link } = setupPaginatedCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; + function App() { + useTrackRenders(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query); - const partialQuery = gql` - query { - character { - id - } - } - `; - const mocks = [ - { - request: { query: fullQuery }, - result: { data: { character: { id: "1", name: "Doctor Strange" } } }, - }, - ]; - - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: DeepPartial; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + return ( + <> + + }> + + + + ); + } - const cache = new InMemoryCache(); + renderWithMocks(, { link, wrapper: Profiler }); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - }); + { + const { renderedComponents } = await Profiler.takeRender(); - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - function App() { - return ( - - }> - - - - ); - } + await act(() => user.click(screen.getByText("Fetch more"))); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + { + const { renderedComponents } = await Profiler.takeRender(); - function Parent() { - const [queryRef] = useBackgroundQuery(fullQuery, { - fetchPolicy: "no-cache", - returnPartialData: true, - }); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - return ; - } + { + const { snapshot } = await Profiler.takeRender(); + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 3, letter: "C" }, + { __typename: "Letter", position: 4, letter: "D" }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.frames.push({ data, networkStatus, error }); - renders.count++; - return ( - <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - render(); + it("properly uses `updateQuery` when calling `fetchMore`", async () => { + const { query, link } = setupPaginatedCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - expect(renders.suspenseCount).toBe(1); + function App() { + useTrackRenders(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query); - await waitFor(() => { - expect(screen.getByTestId("character-name")).toHaveTextContent( - "Doctor Strange" - ); - }); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + return ( + <> + + }> + + + + ); + } - expect(renders.count).toBe(1); - expect(renders.suspenseCount).toBe(1); + renderWithMocks(, { link, wrapper: Profiler }); - expect(renders.frames).toMatchObject([ - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { - using _consoleSpy = spyOnConsole("warn"); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const query: TypedDocumentNode = gql` - query UserQuery { - greeting - } - `; - const mocks = [ - { - request: { query }, - result: { data: { greeting: "Hello" } }, + { + const { snapshot } = await Profiler.takeRender(); + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + ], }, - ]; + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - renderSuspenseHook( - () => - useBackgroundQuery(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - }), - { mocks } - ); + await act(() => user.click(screen.getByText("Fetch more"))); - expect(console.warn).toHaveBeenCalledTimes(1); - expect(console.warn).toHaveBeenCalledWith( - "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy." - ); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { - interface Data { - character: { - id: string; - name: string; - }; - } + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const fullQuery: TypedDocumentNode = gql` - query { - character { - id - name - } - } - `; + // TODO: Determine why we have this extra render here. + // Possibly related: https://github.com/apollographql/apollo-client/issues/11315 + { + const { snapshot } = await Profiler.takeRender(); - const partialQuery = gql` - query { - character { - id - } - } - `; - const mocks = [ - { - request: { query: fullQuery }, - result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + { __typename: "Letter", position: 3, letter: "C" }, + { __typename: "Letter", position: 4, letter: "D" }, + ], }, - ]; - - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: DeepPartial; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } - const cache = new InMemoryCache(); + { + const { snapshot } = await Profiler.takeRender(); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + { __typename: "Letter", position: 3, letter: "C" }, + { __typename: "Letter", position: 4, letter: "D" }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const client = new ApolloClient({ - link: new MockLink(mocks), - cache, - }); + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - function App() { - return ( - - }> - - - - ); - } + it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + const { query, link } = setupPaginatedCase(); + const user = userEvent.setup(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + letters: concatPagination(), + }, + }, + }, + }), + }); - function Parent() { - const [queryRef] = useBackgroundQuery(fullQuery, { - fetchPolicy: "cache-and-network", - returnPartialData: true, - }); + function App() { + useTrackRenders(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query); - return ; - } + return ( + <> + + }> + + + + ); + } - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.frames.push({ data, networkStatus, error }); - renders.count++; - return ( - <> -
{data.character?.id}
-
{data.character?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } + renderWithClient(, { client, wrapper: Profiler }); - render(); + { + const { renderedComponents } = await Profiler.takeRender(); - expect(renders.suspenseCount).toBe(0); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - // name is not present yet, since it's missing in partial data - expect(screen.getByTestId("character-name")).toHaveTextContent(""); - expect(screen.getByTestId("network-status")).toHaveTextContent("1"); // loading - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - await waitFor(() => { - expect(screen.getByTestId("character-name")).toHaveTextContent( - "Doctor Strange" - ); + { + const { snapshot } = await Profiler.takeRender(); + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); - expect(screen.getByTestId("character-id")).toHaveTextContent("1"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + } - expect(renders.count).toBe(2); - expect(renders.suspenseCount).toBe(0); + await act(() => user.click(screen.getByText("Fetch more"))); - expect(renders.frames).toMatchObject([ - { - data: { character: { id: "1" } }, - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, - }, - ]); - }); + { + const { renderedComponents } = await Profiler.takeRender(); - it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { - const partialQuery = gql` - query ($id: ID!) { - character(id: $id) { - id - } - } - `; + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - const cache = new InMemoryCache(); + // TODO: Determine why we have this extra render here. + // Possibly related: https://github.com/apollographql/apollo-client/issues/11315 + { + const { snapshot } = await Profiler.takeRender(); - cache.writeQuery({ - query: partialQuery, - data: { character: { id: "1" } }, - variables: { id: "1" }, + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + { __typename: "Letter", position: 3, letter: "C" }, + { __typename: "Letter", position: 4, letter: "D" }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - const { renders, mocks, rerender } = renderVariablesIntegrationTest({ - variables: { id: "1" }, - cache, - options: { - fetchPolicy: "cache-and-network", - returnPartialData: true, + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + letters: [ + { __typename: "Letter", position: 1, letter: "A" }, + { __typename: "Letter", position: 2, letter: "B" }, + { __typename: "Letter", position: 3, letter: "C" }, + { __typename: "Letter", position: 4, letter: "D" }, + ], }, + error: undefined, + networkStatus: NetworkStatus.ready, }); + } - expect(renders.suspenseCount).toBe(0); + await expect(Profiler).not.toRerender({ timeout: 50 }); + }); - expect(await screen.findByText("1 - Spider-Man")).toBeInTheDocument(); + it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + offset: number; + }; - rerender({ variables: { id: "2" } }); + interface Todo { + __typename: "Todo"; + id: string; + name: string; + completed: boolean; + } + interface Data { + todos: Todo[]; + } + const user = userEvent.setup(); - expect(await screen.findByText("2 - Black Widow")).toBeInTheDocument(); + const query: TypedDocumentNode = gql` + query TodosQuery($offset: Int!) { + todos(offset: $offset) { + id + name + completed + } + } + `; - expect(renders.count).toBe(3); - expect(renders.suspenseCount).toBe(1); - expect(renders.frames).toMatchObject([ - { - data: { character: { id: "1" } }, - networkStatus: NetworkStatus.loading, - error: undefined, - }, - { - ...mocks[0].result, - networkStatus: NetworkStatus.ready, - error: undefined, + const mocks: MockedResponse[] = [ + { + request: { query, variables: { offset: 0 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, }, - { - ...mocks[1].result, - networkStatus: NetworkStatus.ready, - error: undefined, + delay: 10, + }, + { + request: { query, variables: { offset: 1 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, }, - ]); - }); - - it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { - interface QueryData { - greeting: { - __typename: string; - message?: string; - recipient?: { - __typename: string; - name: string; - }; - }; - } - - const query: TypedDocumentNode = gql` - query { - greeting { - message - ... on Greeting @defer { - recipient { - name - } - } - } - } - `; + delay: 10, + }, + ]; - const link = new MockSubscriptionLink(); - const cache = new InMemoryCache(); + const Profiler = createProfiler({ + initialSnapshot: { + isPending: false, + result: null as UseReadQueryResult | null, + }, + }); + const { SuspenseFallback, ReadQueryHook } = + createDefaultTrackedComponents(Profiler); - // We are intentionally writing partial data to the cache. Supress console - // warnings to avoid unnecessary noise in the test. - { - using _consoleSpy = spyOnConsole("error"); - cache.writeQuery({ - query, - data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + todos: offsetLimitPagination(), }, }, - }); - } - - interface Renders { - errors: Error[]; - errorCount: number; - suspenseCount: number; - count: number; - frames: { - data: DeepPartial; - networkStatus: NetworkStatus; - error: ApolloError | undefined; - }[]; - } - const renders: Renders = { - errors: [], - errorCount: 0, - suspenseCount: 0, - count: 0, - frames: [], - }; + }, + }), + }); - const client = new ApolloClient({ - link, - cache, + function App() { + useTrackRenders(); + const [isPending, startTransition] = React.useTransition(); + const [queryRef, { fetchMore }] = useBackgroundQuery(query, { + variables: { offset: 0 }, }); - function App() { - return ( - - }> - - - - ); - } - - function SuspenseFallback() { - renders.suspenseCount++; - return

Loading

; - } + Profiler.mergeSnapshot({ isPending }); - function Parent() { - const [queryRef] = useBackgroundQuery(query, { - fetchPolicy: "cache-first", - returnPartialData: true, - }); + return ( + <> + + }> + + + + ); + } - return ; - } + renderWithClient(, { client, wrapper: Profiler }); - function Todo({ - queryRef, - }: { - queryRef: QueryReference>; - }) { - const { data, networkStatus, error } = useReadQuery(queryRef); - renders.frames.push({ data, networkStatus, error }); - renders.count++; - return ( - <> -
{data.greeting?.message}
-
{data.greeting?.recipient?.name}
-
{networkStatus}
-
{error?.message || "undefined"}
- - ); - } + { + const { renderedComponents } = await Profiler.takeRender(); - render(); + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } - expect(renders.suspenseCount).toBe(0); - expect(screen.getByTestId("recipient")).toHaveTextContent("Cached Alice"); - // message is not present yet, since it's missing in partial data - expect(screen.getByTestId("message")).toHaveTextContent(""); - expect(screen.getByTestId("network-status")).toHaveTextContent("1"); // loading - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + { + const { snapshot } = await Profiler.takeRender(); - link.simulateResult({ + expect(snapshot).toEqual({ + isPending: false, result: { data: { - greeting: { message: "Hello world", __typename: "Greeting" }, + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], }, - hasNext: true, + error: undefined, + networkStatus: NetworkStatus.ready, }, }); + } - await waitFor(() => { - expect(screen.getByTestId("message")).toHaveTextContent("Hello world"); - }); - expect(screen.getByTestId("recipient")).toHaveTextContent("Cached Alice"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + await act(() => user.click(screen.getByText("Load more"))); + + { + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + const { snapshot, renderedComponents } = await Profiler.takeRender(); - link.simulateResult({ + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: true, result: { - incremental: [ - { - data: { - __typename: "Greeting", - recipient: { name: "Alice", __typename: "Person" }, + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, }, - path: ["greeting"], - }, - ], - hasNext: false, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, }, }); + } - await waitFor(() => { - expect(screen.getByTestId("recipient").textContent).toEqual("Alice"); - }); - expect(screen.getByTestId("message")).toHaveTextContent("Hello world"); - expect(screen.getByTestId("network-status")).toHaveTextContent("7"); // ready - expect(screen.getByTestId("error")).toHaveTextContent("undefined"); + // TODO: Determine why we have this extra render here. This should mimic + // the update in the next render where we see included in the + // rerendered components. + // Possibly related: https://github.com/apollographql/apollo-client/issues/11315 + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); - expect(renders.count).toBe(3); - expect(renders.suspenseCount).toBe(0); - expect(renders.frames).toMatchObject([ - { + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { data: { - greeting: { - __typename: "Greeting", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], }, - networkStatus: NetworkStatus.loading, error: undefined, - }, - { - data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Cached Alice" }, - }, - }, networkStatus: NetworkStatus.ready, - error: undefined, }, - { + }); + } + + { + // Eventually we should see the updated todos content once its done + // suspending. + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot).toEqual({ + isPending: false, + result: { data: { - greeting: { - __typename: "Greeting", - message: "Hello world", - recipient: { __typename: "Person", name: "Alice" }, - }, + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], }, - networkStatus: NetworkStatus.ready, error: undefined, + networkStatus: NetworkStatus.ready, }, - ]); - }); + }); + } + + await expect(Profiler).not.toRerender(); }); +}); - describe.skip("type tests", () => { - it("returns unknown when TData cannot be inferred", () => { - const query = gql` - query { - hello - } - `; +describe.skip("type tests", () => { + it("returns unknown when TData cannot be inferred", () => { + const query = gql` + query { + hello + } + `; + + const [queryRef] = useBackgroundQuery(query); + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + }); - const [queryRef] = useBackgroundQuery(query); - const { data } = useReadQuery(queryRef); + it("disallows wider variables type than specified", () => { + const { query } = setupVariablesCase(); - expectTypeOf(data).toEqualTypeOf(); + useBackgroundQuery(query, { + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, }); + }); - it("disallows wider variables type than specified", () => { - const { query } = useVariablesIntegrationTestCase(); + it("returns TData in default case", () => { + const { query } = setupVariablesCase(); - // @ts-expect-error should not allow wider TVariables type - useBackgroundQuery(query, { variables: { id: "1", foo: "bar" } }); - }); + const [inferredQueryRef] = useBackgroundQuery(query); + const { data: inferred } = useReadQuery(inferredQueryRef); - it("returns TData in default case", () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); - const [inferredQueryRef] = useBackgroundQuery(query); - const { data: inferred } = useReadQuery(inferredQueryRef); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + const { data: explicit } = useReadQuery(explicitQueryRef); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - const { data: explicit } = useReadQuery(explicitQueryRef); + it('returns TData | undefined with errorPolicy: "ignore"', () => { + const { query } = setupVariablesCase(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: "ignore", }); + const { data: inferred } = useReadQuery(inferredQueryRef); - it('returns TData | undefined with errorPolicy: "ignore"', () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); - const [inferredQueryRef] = useBackgroundQuery(query, { - errorPolicy: "ignore", - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + errorPolicy: "ignore", + }); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + const { data: explicit } = useReadQuery(explicitQueryRef); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - errorPolicy: "ignore", - }); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - const { data: explicit } = useReadQuery(explicitQueryRef); + it('returns TData | undefined with errorPolicy: "all"', () => { + const { query } = setupVariablesCase(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: "all", }); + const { data: inferred } = useReadQuery(inferredQueryRef); - it('returns TData | undefined with errorPolicy: "all"', () => { - const { query } = useVariablesIntegrationTestCase(); - - const [inferredQueryRef] = useBackgroundQuery(query, { - errorPolicy: "all", - }); - const { data: inferred } = useReadQuery(inferredQueryRef); - - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); - - const [explicitQueryRef] = useBackgroundQuery(query, { - errorPolicy: "all", - }); - const { data: explicit } = useReadQuery(explicitQueryRef); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [explicitQueryRef] = useBackgroundQuery(query, { + errorPolicy: "all", }); + const { data: explicit } = useReadQuery(explicitQueryRef); - it('returns TData with errorPolicy: "none"', () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - const [inferredQueryRef] = useBackgroundQuery(query, { - errorPolicy: "none", - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + it('returns TData with errorPolicy: "none"', () => { + const { query } = setupVariablesCase(); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf(); + const [inferredQueryRef] = useBackgroundQuery(query, { + errorPolicy: "none", + }); + const { data: inferred } = useReadQuery(inferredQueryRef); - const [explicitQueryRef] = useBackgroundQuery(query, { - errorPolicy: "none", - }); - const { data: explicit } = useReadQuery(explicitQueryRef); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [explicitQueryRef] = useBackgroundQuery(query, { + errorPolicy: "none", }); + const { data: explicit } = useReadQuery(explicitQueryRef); - it("returns DeepPartial with returnPartialData: true", () => { - const { query } = useVariablesIntegrationTestCase(); - - const [inferredQueryRef] = useBackgroundQuery(query, { - returnPartialData: true, - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - expectTypeOf(inferred).toEqualTypeOf>(); - expectTypeOf(inferred).not.toEqualTypeOf(); + it("returns DeepPartial with returnPartialData: true", () => { + const { query } = setupVariablesCase(); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: true, - }); + const [inferredQueryRef] = useBackgroundQuery(query, { + returnPartialData: true, + }); + const { data: inferred } = useReadQuery(inferredQueryRef); - const { data: explicit } = useReadQuery(explicitQueryRef); + expectTypeOf(inferred).toEqualTypeOf>(); + expectTypeOf(inferred).not.toEqualTypeOf(); - expectTypeOf(explicit).toEqualTypeOf>(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, }); - it("returns TData with returnPartialData: false", () => { - const { query } = useVariablesIntegrationTestCase(); + const { data: explicit } = useReadQuery(explicitQueryRef); - const [inferredQueryRef] = useBackgroundQuery(query, { - returnPartialData: false, - }); - const { data: inferred } = useReadQuery(inferredQueryRef); - - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf< - DeepPartial - >(); - - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: false, - }); + expectTypeOf(explicit).toEqualTypeOf>(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - const { data: explicit } = useReadQuery(explicitQueryRef); + it("returns TData with returnPartialData: false", () => { + const { query } = setupVariablesCase(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf< - DeepPartial - >(); + const [inferredQueryRef] = useBackgroundQuery(query, { + returnPartialData: false, }); + const { data: inferred } = useReadQuery(inferredQueryRef); - it("returns TData when passing an option that does not affect TData", () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf>(); - const [inferredQueryRef] = useBackgroundQuery(query, { - fetchPolicy: "no-cache", - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: false, + }); - expectTypeOf(inferred).toEqualTypeOf(); - expectTypeOf(inferred).not.toEqualTypeOf< - DeepPartial - >(); + const { data: explicit } = useReadQuery(explicitQueryRef); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - fetchPolicy: "no-cache", - }); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf>(); + }); - const { data: explicit } = useReadQuery(explicitQueryRef); + it("returns TData when passing an option that does not affect TData", () => { + const { query } = setupVariablesCase(); - expectTypeOf(explicit).toEqualTypeOf(); - expectTypeOf(explicit).not.toEqualTypeOf< - DeepPartial - >(); + const [inferredQueryRef] = useBackgroundQuery(query, { + fetchPolicy: "no-cache", }); + const { data: inferred } = useReadQuery(inferredQueryRef); - it("handles combinations of options", () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(inferred).toEqualTypeOf(); + expectTypeOf(inferred).not.toEqualTypeOf>(); - const [inferredPartialDataIgnoreQueryRef] = useBackgroundQuery(query, { - returnPartialData: true, - errorPolicy: "ignore", - }); - const { data: inferredPartialDataIgnore } = useReadQuery( - inferredPartialDataIgnoreQueryRef - ); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: "no-cache", + }); - expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< - DeepPartial | undefined - >(); - expectTypeOf( - inferredPartialDataIgnore - ).not.toEqualTypeOf(); - - const [explicitPartialDataIgnoreQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: true, - errorPolicy: "ignore", - }); + const { data: explicit } = useReadQuery(explicitQueryRef); - const { data: explicitPartialDataIgnore } = useReadQuery( - explicitPartialDataIgnoreQueryRef - ); + expectTypeOf(explicit).toEqualTypeOf(); + expectTypeOf(explicit).not.toEqualTypeOf>(); + }); - expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< - DeepPartial | undefined - >(); - expectTypeOf( - explicitPartialDataIgnore - ).not.toEqualTypeOf(); + it("handles combinations of options", () => { + const { query } = setupVariablesCase(); - const [inferredPartialDataNoneQueryRef] = useBackgroundQuery(query, { - returnPartialData: true, - errorPolicy: "none", - }); + const [inferredPartialDataIgnoreQueryRef] = useBackgroundQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); + const { data: inferredPartialDataIgnore } = useReadQuery( + inferredPartialDataIgnoreQueryRef + ); - const { data: inferredPartialDataNone } = useReadQuery( - inferredPartialDataNoneQueryRef - ); + expectTypeOf(inferredPartialDataIgnore).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf( + inferredPartialDataIgnore + ).not.toEqualTypeOf(); + + const [explicitPartialDataIgnoreQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); - expectTypeOf(inferredPartialDataNone).toEqualTypeOf< - DeepPartial - >(); - expectTypeOf( - inferredPartialDataNone - ).not.toEqualTypeOf(); - - const [explicitPartialDataNoneQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - returnPartialData: true, - errorPolicy: "none", - }); + const { data: explicitPartialDataIgnore } = useReadQuery( + explicitPartialDataIgnoreQueryRef + ); - const { data: explicitPartialDataNone } = useReadQuery( - explicitPartialDataNoneQueryRef - ); + expectTypeOf(explicitPartialDataIgnore).toEqualTypeOf< + DeepPartial | undefined + >(); + expectTypeOf( + explicitPartialDataIgnore + ).not.toEqualTypeOf(); - expectTypeOf(explicitPartialDataNone).toEqualTypeOf< - DeepPartial - >(); - expectTypeOf( - explicitPartialDataNone - ).not.toEqualTypeOf(); + const [inferredPartialDataNoneQueryRef] = useBackgroundQuery(query, { + returnPartialData: true, + errorPolicy: "none", }); - it("returns correct TData type when combined options that do not affect TData", () => { - const { query } = useVariablesIntegrationTestCase(); + const { data: inferredPartialDataNone } = useReadQuery( + inferredPartialDataNoneQueryRef + ); - const [inferredQueryRef] = useBackgroundQuery(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - errorPolicy: "none", - }); - const { data: inferred } = useReadQuery(inferredQueryRef); + expectTypeOf(inferredPartialDataNone).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf( + inferredPartialDataNone + ).not.toEqualTypeOf(); + + const [explicitPartialDataNoneQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + returnPartialData: true, + errorPolicy: "none", + }); - expectTypeOf(inferred).toEqualTypeOf>(); - expectTypeOf(inferred).not.toEqualTypeOf(); + const { data: explicitPartialDataNone } = useReadQuery( + explicitPartialDataNoneQueryRef + ); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { - fetchPolicy: "no-cache", - returnPartialData: true, - errorPolicy: "none", - }); + expectTypeOf(explicitPartialDataNone).toEqualTypeOf< + DeepPartial + >(); + expectTypeOf( + explicitPartialDataNone + ).not.toEqualTypeOf(); + }); - const { data: explicit } = useReadQuery(explicitQueryRef); + it("returns correct TData type when combined options that do not affect TData", () => { + const { query } = setupVariablesCase(); - expectTypeOf(explicit).toEqualTypeOf>(); - expectTypeOf(explicit).not.toEqualTypeOf(); + const [inferredQueryRef] = useBackgroundQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + const { data: inferred } = useReadQuery(inferredQueryRef); + + expectTypeOf(inferred).toEqualTypeOf>(); + expectTypeOf(inferred).not.toEqualTypeOf(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", }); - it("returns QueryReference | undefined when `skip` is present", () => { - const { query } = useVariablesIntegrationTestCase(); + const { data: explicit } = useReadQuery(explicitQueryRef); - const [inferredQueryRef] = useBackgroundQuery(query, { - skip: true, - }); + expectTypeOf(explicit).toEqualTypeOf>(); + expectTypeOf(explicit).not.toEqualTypeOf(); + }); - expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference | undefined - >(); - expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference - >(); - - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, { skip: true }); - - expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference | undefined - >(); - expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference - >(); - - // TypeScript is too smart and using a `const` or `let` boolean variable - // for the `skip` option results in a false positive. Using an options - // object allows us to properly check for a dynamic case. - const options = { - skip: true, - }; + it("returns QueryReference | undefined when `skip` is present", () => { + const { query } = setupVariablesCase(); - const [dynamicQueryRef] = useBackgroundQuery(query, { - skip: options.skip, - }); + const [inferredQueryRef] = useBackgroundQuery(query, { + skip: true, + }); + + expectTypeOf(inferredQueryRef).toEqualTypeOf< + QueryReference | undefined + >(); + expectTypeOf(inferredQueryRef).not.toEqualTypeOf< + QueryReference + >(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { skip: true }); + + expectTypeOf(explicitQueryRef).toEqualTypeOf< + QueryReference | undefined + >(); + expectTypeOf(explicitQueryRef).not.toEqualTypeOf< + QueryReference + >(); + + // TypeScript is too smart and using a `const` or `let` boolean variable + // for the `skip` option results in a false positive. Using an options + // object allows us to properly check for a dynamic case. + const options = { + skip: true, + }; - expectTypeOf(dynamicQueryRef).toEqualTypeOf< - QueryReference | undefined - >(); - expectTypeOf(dynamicQueryRef).not.toEqualTypeOf< - QueryReference - >(); + const [dynamicQueryRef] = useBackgroundQuery(query, { + skip: options.skip, }); - it("returns `undefined` when using `skipToken` unconditionally", () => { - const { query } = useVariablesIntegrationTestCase(); + expectTypeOf(dynamicQueryRef).toEqualTypeOf< + QueryReference | undefined + >(); + expectTypeOf(dynamicQueryRef).not.toEqualTypeOf< + QueryReference + >(); + }); - const [inferredQueryRef] = useBackgroundQuery(query, skipToken); + it("returns `undefined` when using `skipToken` unconditionally", () => { + const { query } = setupVariablesCase(); - expectTypeOf(inferredQueryRef).toEqualTypeOf(); - expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference | undefined - >(); + const [inferredQueryRef] = useBackgroundQuery(query, skipToken); - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, skipToken); + expectTypeOf(inferredQueryRef).toEqualTypeOf(); + expectTypeOf(inferredQueryRef).not.toEqualTypeOf< + QueryReference | undefined + >(); - expectTypeOf(explicitQueryRef).toEqualTypeOf(); - expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference | undefined - >(); - }); + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, skipToken); - it("returns QueryReference | undefined when using conditional `skipToken`", () => { - const { query } = useVariablesIntegrationTestCase(); - const options = { - skip: true, - }; + expectTypeOf(explicitQueryRef).toEqualTypeOf(); + expectTypeOf(explicitQueryRef).not.toEqualTypeOf< + QueryReference | undefined + >(); + }); - const [inferredQueryRef] = useBackgroundQuery( - query, - options.skip ? skipToken : undefined - ); + it("returns QueryReference | undefined when using conditional `skipToken`", () => { + const { query } = setupVariablesCase(); + const options = { + skip: true, + }; - expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference | undefined - >(); - expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference - >(); - - const [explicitQueryRef] = useBackgroundQuery< - VariablesCaseData, - VariablesCaseVariables - >(query, options.skip ? skipToken : undefined); - - expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference | undefined - >(); - expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference - >(); - }); - - it("returns QueryReference> | undefined when using `skipToken` with `returnPartialData`", () => { - const { query } = useVariablesIntegrationTestCase(); - const options = { - skip: true, - }; + const [inferredQueryRef] = useBackgroundQuery( + query, + options.skip ? skipToken : undefined + ); - const [inferredQueryRef] = useBackgroundQuery( - query, - options.skip ? skipToken : { returnPartialData: true } - ); + expectTypeOf(inferredQueryRef).toEqualTypeOf< + QueryReference | undefined + >(); + expectTypeOf(inferredQueryRef).not.toEqualTypeOf< + QueryReference + >(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, options.skip ? skipToken : undefined); + + expectTypeOf(explicitQueryRef).toEqualTypeOf< + QueryReference | undefined + >(); + expectTypeOf(explicitQueryRef).not.toEqualTypeOf< + QueryReference + >(); + }); - expectTypeOf(inferredQueryRef).toEqualTypeOf< - QueryReference> | undefined - >(); - expectTypeOf(inferredQueryRef).not.toEqualTypeOf< - QueryReference - >(); + it("returns QueryReference> | undefined when using `skipToken` with `returnPartialData`", () => { + const { query } = setupVariablesCase(); + const options = { + skip: true, + }; - const [explicitQueryRef] = useBackgroundQuery( - query, - options.skip ? skipToken : { returnPartialData: true } - ); + const [inferredQueryRef] = useBackgroundQuery( + query, + options.skip ? skipToken : { returnPartialData: true } + ); - expectTypeOf(explicitQueryRef).toEqualTypeOf< - QueryReference> | undefined - >(); - expectTypeOf(explicitQueryRef).not.toEqualTypeOf< - QueryReference - >(); - }); + expectTypeOf(inferredQueryRef).toEqualTypeOf< + | QueryReference, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(inferredQueryRef).not.toEqualTypeOf< + QueryReference + >(); + + const [explicitQueryRef] = useBackgroundQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, options.skip ? skipToken : { returnPartialData: true }); + + expectTypeOf(explicitQueryRef).toEqualTypeOf< + | QueryReference, VariablesCaseVariables> + | undefined + >(); + expectTypeOf(explicitQueryRef).not.toEqualTypeOf< + QueryReference + >(); }); }); diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx new file mode 100644 index 00000000000..68ef6a7e9a7 --- /dev/null +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -0,0 +1,5079 @@ +import React, { Suspense, useState } from "react"; +import { + act, + render, + screen, + renderHook, + waitFor, + RenderOptions, +} from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { ErrorBoundary as ReactErrorBoundary } from "react-error-boundary"; +import { expectTypeOf } from "expect-type"; +import { GraphQLError } from "graphql"; +import { + gql, + ApolloError, + ApolloClient, + ErrorPolicy, + NetworkStatus, + TypedDocumentNode, + ApolloLink, + Observable, + OperationVariables, + RefetchWritePolicy, +} from "../../../core"; +import { + MockedProvider, + MockedProviderProps, + MockedResponse, + MockLink, + MockSubscriptionLink, + wait, +} from "../../../testing"; +import { + concatPagination, + offsetLimitPagination, + DeepPartial, +} from "../../../utilities"; +import { useLoadableQuery } from "../useLoadableQuery"; +import type { UseReadQueryResult } from "../useReadQuery"; +import { useReadQuery } from "../useReadQuery"; +import { ApolloProvider } from "../../context"; +import { InMemoryCache } from "../../../cache"; +import { LoadableQueryHookFetchPolicy } from "../../types/types"; +import { QueryReference } from "../../../react"; +import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; +import invariant, { InvariantError } from "ts-invariant"; +import { + Profiler, + SimpleCaseData, + createProfiler, + setupSimpleCase, + spyOnConsole, + useTrackRenders, +} from "../../../testing/internal"; + +afterEach(() => { + jest.useRealTimers(); +}); + +interface SimpleQueryData { + greeting: string; +} + +function useSimpleQueryCase() { + const query: TypedDocumentNode = gql` + query GreetingQuery { + greeting + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + delay: 10, + }, + ]; + + return { query, mocks }; +} + +interface VariablesCaseData { + character: { + id: string; + name: string; + }; +} + +interface VariablesCaseVariables { + id: string; +} + +function useVariablesQueryCase() { + 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 PaginatedQueryData { + letters: { + letter: string; + position: number; + }[]; +} + +interface PaginatedQueryVariables { + limit?: number; + offset?: number; +} + +function usePaginatedQueryCase() { + const query: TypedDocumentNode = + gql` + query letters($limit: Int, $offset: Int) { + letters(limit: $limit) { + letter + position + } + } + `; + + const data = "ABCDEFG" + .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); + }); + }); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + return { query, link, client }; +} + +function createDefaultProfiler() { + return createProfiler({ + initialSnapshot: { + error: null as Error | null, + result: null as UseReadQueryResult | null, + }, + skipNonTrackingRenders: true, + }); +} + +function createDefaultProfiledComponents< + Snapshot extends { + result: UseReadQueryResult | null; + error?: Error | null; + }, + TData = Snapshot["result"] extends UseReadQueryResult | null ? + TData + : unknown, +>(profiler: Profiler) { + function SuspenseFallback() { + useTrackRenders(); + return

Loading

; + } + + function ReadQueryHook({ queryRef }: { queryRef: QueryReference }) { + useTrackRenders(); + profiler.mergeSnapshot({ + result: useReadQuery(queryRef), + } as Partial); + + return null; + } + + function ErrorFallback({ error }: { error: Error }) { + useTrackRenders(); + profiler.mergeSnapshot({ error } as Partial); + + return
Oops
; + } + + function ErrorBoundary({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + return { + SuspenseFallback, + ReadQueryHook, + ErrorFallback, + ErrorBoundary, + }; +} + +function renderWithMocks( + ui: React.ReactElement, + { + wrapper: Wrapper = React.Fragment, + ...props + }: MockedProviderProps & { wrapper?: RenderOptions["wrapper"] } +) { + const user = userEvent.setup(); + + const utils = render(ui, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + return { ...utils, user }; +} + +function renderWithClient( + ui: React.ReactElement, + options: { client: ApolloClient; wrapper?: RenderOptions["wrapper"] } +) { + const { client, wrapper: Wrapper = React.Fragment } = options; + const user = userEvent.setup(); + + const utils = render(ui, { + wrapper: ({ children }) => ( + + {children} + + ), + }); + + return { ...utils, user }; +} + +it("loads a query and suspends when the load query function is called", async () => { + const { query, mocks } = useSimpleQueryCase(); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + } +}); + +it("loads a query with variables and suspends by passing variables to the loadQuery function", async () => { + const { query, mocks } = useVariablesQueryCase(); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } + + await act(() => user.click(screen.getByText("Load query"))); + + { + 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: { character: { id: "1", name: "Spider-Man" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("tears down the query on unmount", async () => { + const { query, mocks } = useSimpleQueryCase(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user, unmount } = renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + unmount(); + + // We need to wait a tick since the cleanup is run in a setTimeout to + // prevent strict mode bugs. + await wait(0); + + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +it("auto disposes of the queryRef if not used within timeout", async () => { + jest.useFakeTimers(); + const { query } = setupSimpleCase(); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ link, cache: new InMemoryCache() }); + + const { result } = renderHook(() => useLoadableQuery(query, { client })); + const [loadQuery] = result.current; + + act(() => loadQuery()); + const [, queryRef] = result.current; + + expect(queryRef!).not.toBeDisposed(); + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + await act(async () => { + link.simulateResult({ result: { data: { greeting: "Hello" } } }, true); + // Ensure simulateResult will deliver the result since its wrapped with + // setTimeout + await jest.advanceTimersByTimeAsync(10); + }); + + jest.advanceTimersByTime(30_000); + + expect(queryRef!).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +it("auto disposes of the queryRef if not used within configured timeout", async () => { + jest.useFakeTimers(); + const { query } = setupSimpleCase(); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + defaultOptions: { + react: { + suspense: { + autoDisposeTimeoutMs: 5000, + }, + }, + }, + }); + + const { result } = renderHook(() => useLoadableQuery(query, { client })); + const [loadQuery] = result.current; + + act(() => loadQuery()); + const [, queryRef] = result.current; + + expect(queryRef!).not.toBeDisposed(); + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + await act(async () => { + link.simulateResult({ result: { data: { greeting: "Hello" } } }, true); + // Ensure simulateResult will deliver the result since its wrapped with + // setTimeout + await jest.advanceTimersByTimeAsync(10); + }); + + jest.advanceTimersByTime(5000); + + expect(queryRef!).toBeDisposed(); + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); +}); + +it("will resubscribe after disposed when mounting useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + defaultOptions: { + react: { + suspense: { + // Set this to something really low to avoid fake timers + autoDisposeTimeoutMs: 20, + }, + }, + }, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(false); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + + }> + {show && queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(query); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } + + // Wait long enough for auto dispose to kick in + await wait(50); + + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + await act(() => user.click(screen.getByText("Toggle"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + 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, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it("auto resubscribes when mounting useReadQuery after naturally disposed by useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + + }> + {show && queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + await act(() => user.click(screen.getByText("Load query"))); + + expect(client.getObservableQueries().size).toBe(1); + expect(client).toHaveSuspenseCacheEntryUsing(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(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(client.getObservableQueries().size).toBe(0); + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + await act(() => user.click(toggleButton)); + + expect(client.getObservableQueries().size).toBe(1); + // Here we don't expect a suspense cache entry because we previously disposed + // of it and did not call `loadQuery` again, which would normally add it to + // the suspense cache + expect(client).not.toHaveSuspenseCacheEntryUsing(query); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + 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, + }); + } + + await expect(Profiler).not.toRerender({ timeout: 50 }); +}); + +it("changes variables on a query and resuspends when passing new variables to the loadQuery function", async () => { + const { query, mocks } = useVariablesQueryCase(); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const App = () => { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + }; + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + } + + await act(() => user.click(screen.getByText("Load 1st character"))); + + { + 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: { character: { id: "1", name: "Spider-Man" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await act(() => user.click(screen.getByText("Load 2nd character"))); + + { + 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: { character: { id: "2", name: "Black Widow" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("resets the `queryRef` to null and disposes of it when calling the `reset` function", async () => { + const { query, mocks } = useSimpleQueryCase(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { reset }] = useLoadableQuery(query); + + // Resetting the result allows us to detect when ReadQueryHook is unmounted + // since it won't render and overwrite the `null` + Profiler.mergeSnapshot({ result: null }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Reset query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toBeNull(); + } + + // Since dispose is called in a setTimeout, we need to wait a tick before + // checking to see if the query ref was properly disposed + await wait(0); + + expect(client.getObservableQueries().size).toBe(0); +}); + +it("allows the client to be overridden", async () => { + const { query } = useSimpleQueryCase(); + + const globalClient = new ApolloClient({ + link: new MockLink([ + { + request: { query }, + result: { data: { greeting: "global hello" } }, + delay: 10, + }, + ]), + cache: new InMemoryCache(), + }); + + const localClient = new ApolloClient({ + link: new MockLink([ + { + request: { query }, + result: { data: { greeting: "local hello" } }, + delay: 10, + }, + ]), + cache: new InMemoryCache(), + }); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + client: localClient, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { + client: globalClient, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "local hello" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); +}); + +it("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 Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + context: { valueA: "A", valueB: "B" }, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { context: { valueA: "A", valueB: "B" } }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); +}); + +it('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 Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + canonizeResults: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + 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]); +}); + +it("can disable canonical results when the cache's canonizeResults setting is true", async () => { + interface Result { + __typename: string; + value: number; + } + + interface QueryData { + results: Result[]; + } + + 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 Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + canonizeResults: false, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { cache, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + 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]); +}); + +it("returns initial cache data followed by network data when the fetch policy is `cache-and-network`", async () => { + type QueryData = { hello: string }; + const query: TypedDocumentNode = gql` + query { + hello + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ link, cache }); + + cache.writeQuery({ query, data: { hello: "from cache" } }); + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-and-network", + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { hello: "from cache" }, + networkStatus: NetworkStatus.loading, + error: undefined, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + } +}); + +it("all data is present in the cache, no network request is made", async () => { + const query = gql` + query { + hello + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ + link, + cache, + }); + + cache.writeQuery({ query, data: { hello: "from cache" } }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { hello: "from cache" }, + networkStatus: NetworkStatus.ready, + error: undefined, + }); + + await expect(Profiler).not.toRerender(); +}); + +it("partial data is present in the cache so it is ignored and network request is made", async () => { + const query = gql` + { + hello + foo + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link", foo: "bar" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ + link, + cache, + }); + + { + // we expect a "Missing field 'foo' while writing result..." error + // when writing hello to the cache, so we'll silence the console.error + using _consoleSpy = spyOnConsole("error"); + cache.writeQuery({ query, data: { hello: "from cache" } }); + } + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { foo: "bar", hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it("existing data in the cache is ignored when `fetchPolicy` is 'network-only'", async () => { + const query = gql` + query { + hello + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ + link, + cache, + }); + + cache.writeQuery({ query, data: { hello: "from cache" } }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "network-only", + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it("fetches data from the network but does not update the cache when `fetchPolicy` is 'no-cache'", async () => { + const query = gql` + query { + hello + } + `; + const cache = new InMemoryCache(); + const link = new MockLink([ + { + request: { query }, + result: { data: { hello: "from link" } }, + delay: 20, + }, + ]); + + const client = new ApolloClient({ link, cache }); + + cache.writeQuery({ query, data: { hello: "from cache" } }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "no-cache", + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { hello: "from link" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(client.extract()).toEqual({ + ROOT_QUERY: { __typename: "Query", hello: "from cache" }, + }); +}); + +it("works with startTransition to change variables", async () => { + type Variables = { + id: string; + }; + + interface Data { + todo: { + id: string; + name: string; + completed: boolean; + }; + } + + const query: TypedDocumentNode = gql` + query TodoItemQuery($id: ID!) { + todo(id: $id) { + id + name + completed + } + } + `; + + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { todo: { id: "1", name: "Clean room", completed: false } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { + todo: { id: "2", name: "Take out trash", completed: true }, + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache(), + }); + + function SuspenseFallback() { + return

Loading

; + } + + function App() { + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( +
+ + }> + {queryRef && ( + loadQuery({ id })} /> + )} + +
+ ); + } + + function Todo({ + queryRef, + onChange, + }: { + queryRef: QueryReference; + onChange: (id: string) => void; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todo } = data; + + return ( + <> + +
+ {todo.name} + {todo.completed && " (completed)"} +
+ + ); + } + + const { user } = renderWithClient(, { client }); + + await act(() => user.click(screen.getByText("Load first todo"))); + + expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(await screen.findByTestId("todo")).toBeInTheDocument(); + + const todo = screen.getByTestId("todo"); + const button = screen.getByText("Refresh"); + + expect(todo).toHaveTextContent("Clean room"); + + await act(() => user.click(button)); + + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + + // We can ensure this works with isPending from useTransition in the process + expect(todo).toHaveAttribute("aria-busy", "true"); + + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); + + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Take out trash (completed)"); + }); +}); + +it('does not suspend deferred queries with data in the cache and using a "cache-and-network" fetch policy', async () => { + interface Data { + greeting: { + __typename: string; + message: string; + recipient: { name: string; __typename: string }; + }; + } + + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + const client = new ApolloClient({ cache, link }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-and-network", + }); + return ( +
+ + }> + {queryRef && } + +
+ ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load todo"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + message: "Hello cached", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + link.simulateResult({ + result: { + data: { + greeting: { __typename: "Greeting", message: "Hello world" }, + }, + hasNext: 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: "Cached Alice" }, + }, + }, + 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, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("reacts to cache updates", async () => { + const { query, mocks } = useSimpleQueryCase(); + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new MockLink(mocks), + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + client.writeQuery({ + query, + data: { greeting: "Updated Hello" }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Updated Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("applies `errorPolicy` on next fetch when it changes between renders", async () => { + const { query } = useSimpleQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query }, + result: { data: { greeting: "Hello" } }, + delay: 10, + }, + { + request: { query }, + result: { + errors: [new GraphQLError("oops")], + }, + delay: 10, + }, + ]; + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [errorPolicy, setErrorPolicy] = useState("none"); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + errorPolicy, + }); + + return ( + <> + + + + }> + + {queryRef && } + + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Change error policy"))); + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Refetch greeting"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + // Ensure we aren't rendering the error boundary and instead rendering the + // error message in the hook component. + expect(renderedComponents).not.toContain(ErrorFallback); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello" }, + error: new ApolloError({ graphQLErrors: [new GraphQLError("oops")] }), + networkStatus: NetworkStatus.error, + }); + } +}); + +it("applies `context` on next fetch when it changes between renders", async () => { + interface Data { + phase: string; + } + + const query: TypedDocumentNode = gql` + query { + phase + } + `; + + const link = new ApolloLink((operation) => { + return new Observable((subscriber) => { + setTimeout(() => { + subscriber.next({ + data: { + phase: operation.getContext().phase, + }, + }); + subscriber.complete(); + }, 10); + }); + }); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [phase, setPhase] = React.useState("initial"); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + context: { phase }, + }); + + return ( + <> + + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result!.data).toEqual({ + phase: "initial", + }); + } + + await act(() => user.click(screen.getByText("Update context"))); + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result!.data).toEqual({ + phase: "rerender", + }); + } +}); + +// NOTE: We only test the `false` -> `true` path here. If the option changes +// from `true` -> `false`, the data has already been canonized, so it has no +// effect on the output. +it("returns canonical results immediately when `canonizeResults` changes from `false` to `true` between renders", async () => { + interface Result { + __typename: string; + value: number; + } + + interface Data { + 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({ + link: new MockLink([]), + cache, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [canonizeResults, setCanonizeResults] = React.useState(false); + const [loadQuery, queryRef] = useLoadableQuery(query, { + canonizeResults, + }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await Profiler.takeRender(); + const { data } = snapshot.result!; + const resultSet = new Set(data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(data.results.length).toBe(6); + expect(resultSet.size).toBe(6); + expect(values).toEqual([0, 1, 1, 2, 3, 5]); + } + + await act(() => user.click(screen.getByText("Canonize results"))); + + { + const { snapshot } = await Profiler.takeRender(); + const { data } = snapshot.result!; + const resultSet = new Set(data.results); + const values = Array.from(resultSet).map((item) => item.value); + + expect(data.results.length).toBe(6); + expect(resultSet.size).toBe(5); + expect(values).toEqual([0, 1, 2, 3, 5]); + } +}); + +it("applies changed `refetchWritePolicy` to next fetch when changing between renders", async () => { + interface Data { + primes: number[]; + } + + const query: TypedDocumentNode = gql` + query GetPrimes($min: number, $max: number) { + primes(min: $min, max: $max) + } + `; + + 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, + }, + { + request: { query, variables: { min: 30, max: 50 } }, + result: { data: { primes: [31, 37, 41, 43, 47] } }, + 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 client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [refetchWritePolicy, setRefetchWritePolicy] = + React.useState("merge"); + + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + refetchWritePolicy, + }); + + return ( + <> + + + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + const { primes } = snapshot.result!.data; + + expect(primes).toEqual([2, 3, 5, 7, 11]); + expect(mergeParams).toEqual([[undefined, [2, 3, 5, 7, 11]]]); + } + + await act(() => user.click(screen.getByText("Refetch next"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + const { primes } = snapshot.result!.data; + + expect(primes).toEqual([2, 3, 5, 7, 11, 13, 17, 19, 23, 29]); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + ]); + } + + await act(() => user.click(screen.getByText("Change refetch write policy"))); + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Refetch last"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + const { primes } = snapshot.result!.data; + + expect(primes).toEqual([31, 37, 41, 43, 47]); + expect(mergeParams).toEqual([ + [undefined, [2, 3, 5, 7, 11]], + [ + [2, 3, 5, 7, 11], + [13, 17, 19, 23, 29], + ], + [undefined, [31, 37, 41, 43, 47]], + ]); + } +}); + +it("applies `returnPartialData` on next fetch when it changes between renders", async () => { + interface Data { + character: { + __typename: "Character"; + id: string; + name: string; + }; + } + + interface PartialData { + character: { + __typename: "Character"; + id: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + __typename + id + name + } + } + `; + + const partialQuery: TypedDocumentNode = gql` + query { + character { + __typename + id + } + } + `; + + const mocks = [ + { + request: { query: fullQuery }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", + }, + }, + }, + delay: 10, + }, + { + request: { query: fullQuery }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange (refetched)", + }, + }, + }, + delay: 100, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { __typename: "Character", id: "1" } }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [returnPartialData, setReturnPartialData] = React.useState(false); + + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { + returnPartialData, + }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Doctor Strange" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Update partial data"))); + await Profiler.takeRender(); + + cache.modify({ + id: cache.identify({ __typename: "Character", id: "1" }), + fields: { + name: (_, { DELETE }) => DELETE, + }, + }); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1" }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange (refetched)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it("applies updated `fetchPolicy` on next fetch when it changes between renders", async () => { + interface Data { + character: { + __typename: "Character"; + id: string; + name: string; + }; + } + + const query: TypedDocumentNode = gql` + query { + character { + __typename + id + name + } + } + `; + + const mocks = [ + { + request: { query }, + result: { + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", + }, + }, + }, + delay: 10, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query, + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + }); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [fetchPolicy, setFetchPolicy] = + React.useState("cache-first"); + + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + fetchPolicy, + }); + + return ( + <> + + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Change fetch policy"))); + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Doctor Strange", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // Because we switched to a `no-cache` fetch policy, we should not see the + // newly fetched data in the cache after the fetch occured. + expect(cache.readQuery({ query })).toEqual({ + character: { + __typename: "Character", + id: "1", + name: "Doctor Strangecache", + }, + }); +}); + +it("re-suspends when calling `refetch`", async () => { + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Spider-Man" } }, + }, + delay: 20, + }, + // refetch + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Spider-Man (updated)" } }, + }, + delay: 20, + }, + ]; + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, 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: { character: { id: "1", name: "Spider-Man" } }, + 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: { character: { id: "1", name: "Spider-Man (updated)" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("re-suspends when calling `refetch` with new variables", async () => { + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "2" } }, + result: { + data: { character: { id: "2", name: "Captain America" } }, + }, + delay: 10, + }, + ]; + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, 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: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch with ID 2"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "2", name: "Captain America" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("re-suspends multiple times when calling `refetch` multiple times", async () => { + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Spider-Man" } }, + }, + maxUsageCount: 3, + delay: 10, + }, + ]; + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { mocks, 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: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + const button = screen.getByText("Refetch"); + + await act(() => user.click(button)); + + { + 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, + }); + } + + await act(() => user.click(button)); + + { + 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, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("throws errors when errors are returned after calling `refetch`", async () => { + using _consoleSpy = spyOnConsole("error"); + + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + delay: 20, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + delay: 20, + }, + ]; + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + + }> + + {queryRef && } + + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ErrorFallback]); + expect(snapshot.error).toEqual( + new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }) + ); + } +}); + +it('ignores errors returned after calling `refetch` when errorPolicy is set to "ignore"', async () => { + const { query } = useVariablesQueryCase(); + + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + delay: 10, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + delay: 10, + }, + ]; + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + errorPolicy: "ignore", + }); + + return ( + <> + + + }> + + {queryRef && } + + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toStrictEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.error).toBeNull(); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + expect(renderedComponents).not.toContain(ErrorFallback); + } + + await expect(Profiler).not.toRerender(); +}); + +it('returns errors after calling `refetch` when errorPolicy is set to "all"', async () => { + const { query } = useVariablesQueryCase(); + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + delay: 20, + }, + { + request: { query, variables: { id: "1" } }, + result: { + errors: [new GraphQLError("Something went wrong")], + }, + delay: 20, + }, + ]; + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + errorPolicy: "all", + }); + + return ( + <> + + + }> + + {queryRef && } + + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).not.toContain(ErrorFallback); + expect(snapshot.error).toBeNull(); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it('handles partial data results after calling `refetch` when errorPolicy is set to "all"', async () => { + const { query } = useVariablesQueryCase(); + + const mocks = [ + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: "Captain Marvel" } }, + }, + delay: 20, + }, + { + request: { query, variables: { id: "1" } }, + result: { + data: { character: { id: "1", name: null } }, + errors: [new GraphQLError("Something went wrong")], + }, + delay: 20, + }, + ]; + + const Profiler = createDefaultProfiler(); + + const { SuspenseFallback, ReadQueryHook, ErrorBoundary, ErrorFallback } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + errorPolicy: "all", + }); + + return ( + <> + + + }> + + {queryRef && } + + + + ); + } + + const { user } = renderWithMocks(, { mocks, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Captain Marvel" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Refetch"))); + await Profiler.takeRender(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).not.toContain(ErrorFallback); + expect(snapshot.error).toBeNull(); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: null } }, + error: new ApolloError({ + graphQLErrors: [new GraphQLError("Something went wrong")], + }), + networkStatus: NetworkStatus.error, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("`refetch` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + id: string; + }; + + interface Data { + todo: { + id: string; + name: string; + completed: boolean; + }; + } + + 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, + }, + ]; + + function SuspenseFallback() { + return

Loading

; + } + + function App() { + const [id, setId] = React.useState("1"); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && ( + + )} + + + ); + } + + function Todo({ + queryRef, + refetch, + }: { + refetch: RefetchFunction; + queryRef: QueryReference; + onChange: (id: string) => void; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todo } = data; + + return ( + <> + +
+ {todo.name} + {todo.completed && " (completed)"} +
+ + ); + } + + const { user } = renderWithMocks(, { mocks }); + + await act(() => user.click(screen.getByText("Load query"))); + + expect(screen.getByText("Loading")).toBeInTheDocument(); + expect(await screen.findByTestId("todo")).toBeInTheDocument(); + + const todo = screen.getByTestId("todo"); + const button = screen.getByText("Refresh"); + + expect(todo).toHaveTextContent("Clean room"); + + await act(() => user.click(button)); + + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + + // We can ensure this works with isPending from useTransition in the process + expect(todo).toHaveAttribute("aria-busy", "true"); + + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo).toHaveTextContent("Clean room"); + + // Eventually we should see the updated todo content once its done + // suspending. + await waitFor(() => { + expect(todo).toHaveTextContent("Clean room (completed)"); + }); +}); + +it("re-suspends when calling `fetchMore` with different variables", async () => { + const { query, client } = usePaginatedQueryCase(); + + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + 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("Fetch more"))); + + { + 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: { + letters: [ + { letter: "C", position: 3 }, + { letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("properly uses `updateQuery` when calling `fetchMore`", async () => { + const { query, client } = usePaginatedQueryCase(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + 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("Fetch more"))); + await Profiler.takeRender(); + + { + 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, + }); + } + + // TODO investigate: this test highlights a React render + // that actually doesn't rerender any user-provided components + // so we need to use `skipNonTrackingRenders` + await expect(Profiler).not.toRerender(); +}); + +it("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + const { query, link } = usePaginatedQueryCase(); + const Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + letters: concatPagination(), + }, + }, + }, + }), + }); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + await Profiler.takeRender(); + + { + 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("Fetch more"))); + await Profiler.takeRender(); + + { + 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, + }); + } + + // TODO investigate: this test highlights a React render + // that actually doesn't rerender any user-provided components + // so we need to use `skipNonTrackingRenders` + await expect(Profiler).not.toRerender(); +}); + +it("`fetchMore` works with startTransition to allow React to show stale UI until finished suspending", async () => { + type Variables = { + offset: number; + }; + + interface Todo { + __typename: "Todo"; + id: string; + name: string; + completed: boolean; + } + interface Data { + todos: Todo[]; + } + + const query: TypedDocumentNode = gql` + query TodosQuery($offset: Int!) { + todos(offset: $offset) { + id + name + completed + } + } + `; + + const mocks: MockedResponse[] = [ + { + request: { query, variables: { offset: 0 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "1", + name: "Clean room", + completed: false, + }, + ], + }, + }, + delay: 10, + }, + { + request: { query, variables: { offset: 1 } }, + result: { + data: { + todos: [ + { + __typename: "Todo", + id: "2", + name: "Take out trash", + completed: true, + }, + ], + }, + }, + delay: 10, + }, + ]; + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache: new InMemoryCache({ + typePolicies: { + Query: { + fields: { + todos: offsetLimitPagination(), + }, + }, + }, + }), + }); + + function SuspenseFallback() { + return

Loading

; + } + + function App() { + const [loadQuery, queryRef, { fetchMore }] = useLoadableQuery(query); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + function Todo({ + queryRef, + fetchMore, + }: { + fetchMore: FetchMoreFunction; + queryRef: QueryReference; + }) { + const { data } = useReadQuery(queryRef); + const [isPending, startTransition] = React.useTransition(); + const { todos } = data; + + return ( + <> + +
+ {todos.map((todo) => ( +
+ {todo.name} + {todo.completed && " (completed)"} +
+ ))} +
+ + ); + } + + const { user } = renderWithClient(, { client }); + + await act(() => user.click(screen.getByText("Load query"))); + + expect(screen.getByText("Loading")).toBeInTheDocument(); + + expect(await screen.findByTestId("todos")).toBeInTheDocument(); + + const todos = screen.getByTestId("todos"); + const todo1 = screen.getByTestId("todo:1"); + const button = screen.getByText("Load more"); + + expect(todo1).toBeInTheDocument(); + + await act(() => user.click(button)); + + // startTransition will avoid rendering the suspense fallback for already + // revealed content if the state update inside the transition causes the + // component to suspend. + // + // Here we should not see the suspense fallback while the component suspends + // until the todo is finished loading. Seeing the suspense fallback is an + // indication that we are suspending the component too late in the process. + expect(screen.queryByText("Loading")).not.toBeInTheDocument(); + + // We can ensure this works with isPending from useTransition in the process + expect(todos).toHaveAttribute("aria-busy", "true"); + + // Ensure we are showing the stale UI until the new todo has loaded + expect(todo1).toHaveTextContent("Clean room"); + + // Eventually we should see the updated todos content once its done + // suspending. + await waitFor(() => { + expect(screen.getByTestId("todo:2")).toHaveTextContent( + "Take out trash (completed)" + ); + expect(todo1).toHaveTextContent("Clean room"); + }); +}); + +it('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 Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query, { + refetchWritePolicy: "merge", + }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + 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(); +}); + +it('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 Profiler = createDefaultProfiler(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const client = new ApolloClient({ + link: new MockLink(mocks), + cache, + }); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef, { refetch }] = useLoadableQuery(query); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial load + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + 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]], + ]); + } +}); + +it('does not suspend when partial data is in the cache and using a "cache-first" fetch policy with returnPartialData', async () => { + interface Data { + character: { + id: string; + name: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + delay: 20, + }, + ]; + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + const client = new ApolloClient({ link: new MockLink(mocks), cache }); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial load + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + } + + await expect(Profiler).not.toRerender(); +}); + +it('suspends and does not use partial data from other variables in the cache when changing variables and using a "cache-first" fetch policy with returnPartialData: true', async () => { + const { query, mocks } = useVariablesQueryCase(); + + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id + } + } + `; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + variables: { id: "1" }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + } + + await act(() => user.click(screen.getByText("Change variables"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "2", name: "Black Widow" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + } + + await expect(Profiler).not.toRerender(); +}); + +it('suspends when partial data is in the cache and using a "network-only" fetch policy with returnPartialData', async () => { + interface Data { + character: { + id: string; + name: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + delay: 10, + }, + ]; + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { + fetchPolicy: "network-only", + returnPartialData: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { + mocks, + cache, + 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: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it('suspends when partial data is in the cache and using a "no-cache" fetch policy with returnPartialData', async () => { + using _consoleSpy = spyOnConsole("warn"); + + interface Data { + character: { + id: string; + name: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + delay: 10, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { + fetchPolicy: "no-cache", + returnPartialData: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: Profiler, + }); + + // initial load + 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: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it('warns when using returnPartialData with a "no-cache" fetch policy', async () => { + using _consoleSpy = spyOnConsole("warn"); + + const query: TypedDocumentNode = gql` + query UserQuery { + greeting + } + `; + + renderHook( + () => + useLoadableQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Using `returnPartialData` with a `no-cache` fetch policy has no effect. To read partial data from the cache, consider using an alternate fetch policy." + ); +}); + +it('does not suspend when partial data is in the cache and using a "cache-and-network" fetch policy with returnPartialData', async () => { + interface Data { + character: { + id: string; + name: string; + }; + } + + const fullQuery: TypedDocumentNode = gql` + query { + character { + id + name + } + } + `; + + const partialQuery = gql` + query { + character { + id + } + } + `; + const mocks = [ + { + request: { query: fullQuery }, + result: { data: { character: { id: "1", name: "Doctor Strange" } } }, + delay: 20, + }, + ]; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(fullQuery, { + fetchPolicy: "cache-and-network", + returnPartialData: true, + }); + + return ( + <> + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Doctor Strange" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it('suspends and does not use partial data when changing variables and using a "cache-and-network" fetch policy with returnPartialData', async () => { + const { query, mocks } = useVariablesQueryCase(); + + const partialQuery = gql` + query ($id: ID!) { + character(id: $id) { + id + } + } + `; + + const cache = new InMemoryCache(); + + cache.writeQuery({ + query: partialQuery, + data: { character: { id: "1" } }, + variables: { id: "1" }, + }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadQuery, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-and-network", + returnPartialData: true, + }); + + return ( + <> + + + }> + {queryRef && } + + + ); + } + + const { user } = renderWithMocks(, { + mocks, + cache, + wrapper: Profiler, + }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load query"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { id: "1", name: "Spider-Man" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await act(() => user.click(screen.getByText("Change variables"))); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { character: { id: "2", name: "Black Widow" } }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +it('does not suspend deferred queries with partial data in the cache and using a "cache-first" fetch policy with `returnPartialData`', async () => { + interface QueryData { + greeting: { + __typename: string; + message?: string; + recipient?: { + __typename: string; + name: string; + }; + }; + } + + const query: TypedDocumentNode = gql` + query { + greeting { + message + ... on Greeting @defer { + recipient { + name + } + } + } + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + + { + // We are intentionally writing partial data to the cache. Supress console + // warnings to avoid unnecessary noise in the test. + using _consoleSpy = spyOnConsole("error"); + + cache.writeQuery({ + query, + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + }); + } + + const client = new ApolloClient({ link, cache }); + + const Profiler = createDefaultProfiler>(); + const { SuspenseFallback, ReadQueryHook } = + createDefaultProfiledComponents(Profiler); + + function App() { + useTrackRenders(); + const [loadTodo, queryRef] = useLoadableQuery(query, { + fetchPolicy: "cache-first", + returnPartialData: true, + }); + + return ( +
+ + }> + {queryRef && } + +
+ ); + } + + const { user } = renderWithClient(, { client, wrapper: Profiler }); + + // initial render + await Profiler.takeRender(); + + await act(() => user.click(screen.getByText("Load todo"))); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + greeting: { + __typename: "Greeting", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + 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: { + __typename: "Greeting", + message: "Hello world", + recipient: { __typename: "Person", name: "Cached Alice" }, + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + link.simulateResult( + { + result: { + incremental: [ + { + data: { + __typename: "Greeting", + recipient: { name: "Alice", __typename: "Person" }, + }, + 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, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +it("throws when calling loadQuery on first render", async () => { + using _consoleSpy = spyOnConsole("error"); + const { query, mocks } = useSimpleQueryCase(); + + function App() { + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + + return null; + } + + expect(() => renderWithMocks(, { mocks })).toThrow( + new InvariantError( + "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." + ) + ); +}); + +it("throws when calling loadQuery on subsequent render", async () => { + using _consoleSpy = spyOnConsole("error"); + const { query, mocks } = useSimpleQueryCase(); + + let error!: Error; + + function App() { + const [count, setCount] = useState(0); + const [loadQuery] = useLoadableQuery(query); + + if (count === 1) { + loadQuery(); + } + + return ; + } + + const { user } = renderWithMocks( + (error = e)} fallback={
Oops
}> + +
, + { mocks } + ); + + await act(() => user.click(screen.getByText("Load query in render"))); + + expect(error).toEqual( + new InvariantError( + "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." + ) + ); +}); + +it("allows loadQuery to be called in useEffect on first render", async () => { + const { query, mocks } = useSimpleQueryCase(); + + function App() { + const [loadQuery] = useLoadableQuery(query); + + React.useEffect(() => { + loadQuery(); + }, []); + + return null; + } + + expect(() => renderWithMocks(, { mocks })).not.toThrow(); +}); + +describe.skip("type tests", () => { + it("returns unknown when TData cannot be inferred", () => { + const query = gql``; + + const [, queryRef] = useLoadableQuery(query); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + }); + + it("variables are optional and can be anything with an untyped DocumentNode", () => { + const query = gql``; + + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + loadQuery({}); + loadQuery({ foo: "bar" }); + loadQuery({ bar: "baz" }); + }); + + it("variables are optional and can be anything with unspecified TVariables on a TypedDocumentNode", () => { + const query: TypedDocumentNode<{ greeting: string }> = gql``; + + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + loadQuery({}); + loadQuery({ foo: "bar" }); + loadQuery({ bar: "baz" }); + }); + + it("variables are optional when TVariables are empty", () => { + const query: TypedDocumentNode< + { greeting: string }, + Record + > = gql``; + + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + loadQuery({}); + // @ts-expect-error unknown variable + loadQuery({ foo: "bar" }); + }); + + it("does not allow variables when TVariables is `never`", () => { + const query: TypedDocumentNode<{ greeting: string }, never> = gql``; + + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + // @ts-expect-error no variables argument allowed + loadQuery({}); + // @ts-expect-error no variables argument allowed + loadQuery({ foo: "bar" }); + }); + + it("optional variables are optional to loadQuery", () => { + const query: TypedDocumentNode<{ posts: string[] }, { limit?: number }> = + gql``; + + const [loadQuery] = useLoadableQuery(query); + + loadQuery(); + loadQuery({}); + loadQuery({ limit: 10 }); + loadQuery({ + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }); + }); + + it("enforces required variables when TVariables includes required variables", () => { + const query: TypedDocumentNode<{ character: string }, { id: string }> = + gql``; + + const [loadQuery] = useLoadableQuery(query); + + // @ts-expect-error missing variables argument + loadQuery(); + // @ts-expect-error empty variables + loadQuery({}); + loadQuery({ id: "1" }); + loadQuery({ + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }); + }); + + it("requires variables with mixed TVariables", () => { + const query: TypedDocumentNode< + { character: string }, + { id: string; language?: string } + > = gql``; + + const [loadQuery] = useLoadableQuery(query); + + // @ts-expect-error missing variables argument + loadQuery(); + // @ts-expect-error empty variables + loadQuery({}); + loadQuery({ id: "1" }); + // @ts-expect-error missing required variable + loadQuery({ language: "en" }); + loadQuery({ id: "1", language: "en" }); + loadQuery({ + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }); + loadQuery({ + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }); + }); + + it("returns TData in default case", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it('returns TData | undefined with errorPolicy: "ignore"', () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + errorPolicy: "ignore", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "ignore" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it('returns TData | undefined with errorPolicy: "all"', () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + errorPolicy: "all", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "all" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it('returns TData with errorPolicy: "none"', () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + errorPolicy: "none", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { errorPolicy: "none" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it("returns DeepPartial with returnPartialData: true", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + returnPartialData: true, + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + }); + + it("returns TData with returnPartialData: false", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + returnPartialData: false, + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: false }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it("returns TData when passing an option that does not affect TData", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + fetchPolicy: "no-cache", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { fetchPolicy: "no-cache" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf(); + } + }); + + it("handles combinations of options", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: "ignore" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf< + DeepPartial | undefined + >(); + } + + { + const [, queryRef] = useLoadableQuery(query, { + returnPartialData: true, + errorPolicy: "none", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { returnPartialData: true, errorPolicy: "none" }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + }); + + it("returns correct TData type when combined options that do not affect TData", () => { + const { query } = useVariablesQueryCase(); + + { + const [, queryRef] = useLoadableQuery(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + + { + const [, queryRef] = useLoadableQuery< + VariablesCaseData, + VariablesCaseVariables + >(query, { + fetchPolicy: "no-cache", + returnPartialData: true, + errorPolicy: "none", + }); + + invariant(queryRef); + + const { data } = useReadQuery(queryRef); + + expectTypeOf(data).toEqualTypeOf>(); + } + }); +}); diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index 2eff7db1cb5..938781a9890 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, ReactNode, useEffect, useState } from "react"; +import React, { Fragment, ReactNode, useEffect, useRef, useState } from "react"; import { DocumentNode, GraphQLError } from "graphql"; import gql from "graphql-tag"; import { act } from "react-dom/test-utils"; @@ -27,6 +27,7 @@ import { QueryResult } from "../../types/types"; import { useQuery } from "../useQuery"; import { useMutation } from "../useMutation"; import { profileHook, spyOnConsole } from "../../../testing/internal"; +import { useApolloClient } from "../useApolloClient"; describe("useQuery Hook", () => { describe("General use", () => { @@ -2092,6 +2093,200 @@ describe("useQuery Hook", () => { unmount(); result.current.stopPolling(); }); + + describe("should prevent fetches when `skipPollAttempt` returns `false`", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); + }); + + it("when defined as a global default option", async () => { + const skipPollAttempt = jest.fn().mockImplementation(() => false); + + const query = gql` + { + hello + } + `; + const link = mockSingleLink( + { + request: { query }, + result: { data: { hello: "world 1" } }, + }, + { + request: { query }, + result: { data: { hello: "world 2" } }, + }, + { + request: { query }, + result: { data: { hello: "world 3" } }, + } + ); + + const client = new ApolloClient({ + link, + cache: new InMemoryCache(), + defaultOptions: { + watchQuery: { + skipPollAttempt, + }, + }, + }); + + const wrapper = ({ children }: any) => ( + {children} + ); + + const { result } = renderHook( + () => useQuery(query, { pollInterval: 10 }), + { wrapper } + ); + + expect(result.current.loading).toBe(true); + expect(result.current.data).toBe(undefined); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 1" }); + }, + { interval: 1 } + ); + + expect(result.current.loading).toBe(false); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 2" }); + }, + { interval: 1 } + ); + + skipPollAttempt.mockImplementation(() => true); + expect(result.current.loading).toBe(false); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + skipPollAttempt.mockImplementation(() => false); + expect(result.current.loading).toBe(false); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 3" }); + }, + { interval: 1 } + ); + }); + + it("when defined for a single query", async () => { + const skipPollAttempt = jest.fn().mockImplementation(() => false); + + const query = gql` + { + hello + } + `; + const mocks = [ + { + request: { query }, + result: { data: { hello: "world 1" } }, + }, + { + request: { query }, + result: { data: { hello: "world 2" } }, + }, + { + request: { query }, + result: { data: { hello: "world 3" } }, + }, + ]; + + const cache = new InMemoryCache(); + const wrapper = ({ children }: any) => ( + + {children} + + ); + + const { result } = renderHook( + () => + useQuery(query, { + pollInterval: 10, + skipPollAttempt, + }), + { wrapper } + ); + + expect(result.current.loading).toBe(true); + expect(result.current.data).toBe(undefined); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 1" }); + }, + { interval: 1 } + ); + + expect(result.current.loading).toBe(false); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 2" }); + }, + { interval: 1 } + ); + + skipPollAttempt.mockImplementation(() => true); + expect(result.current.loading).toBe(false); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + await jest.advanceTimersByTime(12); + await waitFor( + () => expect(result.current.data).toEqual({ hello: "world 2" }), + { interval: 1 } + ); + + skipPollAttempt.mockImplementation(() => false); + expect(result.current.loading).toBe(false); + + await waitFor( + () => { + expect(result.current.data).toEqual({ hello: "world 3" }); + }, + { interval: 1 } + ); + }); + }); }); describe("Error handling", () => { @@ -4300,6 +4495,138 @@ describe("useQuery Hook", () => { }); }); }); + + it("keeps cache consistency when a call to refetchQueries is interrupted with another query caused by changing variables and the second query returns before the first one", async () => { + const CAR_QUERY_BY_ID = gql` + query Car($id: Int) { + car(id: $id) { + make + model + } + } + `; + + const mocks = { + 1: [ + { + car: { + make: "Audi", + model: "A4", + __typename: "Car", + }, + }, + { + car: { + make: "Audi", + model: "A3", // Changed + __typename: "Car", + }, + }, + ], + 2: [ + { + car: { + make: "Audi", + model: "RS8", + __typename: "Car", + }, + }, + ], + }; + + const link = new ApolloLink( + (operation) => + new Observable((observer) => { + if (operation.variables.id === 1) { + // Queries for this ID return after a delay + setTimeout(() => { + const data = mocks[1].splice(0, 1).pop(); + observer.next({ data }); + observer.complete(); + }, 100); + } else if (operation.variables.id === 2) { + // Queries for this ID return immediately + const data = mocks[2].splice(0, 1).pop(); + observer.next({ data }); + observer.complete(); + } else { + observer.error(new Error("Unexpected query")); + } + }) + ); + + const hookResponse = jest.fn().mockReturnValue(null); + + function Component({ children, id }: any) { + const result = useQuery(CAR_QUERY_BY_ID, { + variables: { id }, + notifyOnNetworkStatusChange: true, + fetchPolicy: "network-only", + }); + const client = useApolloClient(); + const hasRefetchedRef = useRef(false); + + useEffect(() => { + if ( + result.networkStatus === NetworkStatus.ready && + !hasRefetchedRef.current + ) { + client.reFetchObservableQueries(); + hasRefetchedRef.current = true; + } + }, [result.networkStatus]); + + return children(result); + } + + const { rerender } = render( + {hookResponse}, + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await waitFor(() => { + // Resolves as soon as reFetchObservableQueries is + // called, but before the result is returned + expect(hookResponse).toHaveBeenCalledTimes(3); + }); + + rerender({hookResponse}); + + await waitFor(() => { + // All results are returned + expect(hookResponse).toHaveBeenCalledTimes(5); + }); + + expect(hookResponse.mock.calls.map((call) => call[0].data)).toEqual([ + undefined, + { + car: { + __typename: "Car", + make: "Audi", + model: "A4", + }, + }, + { + car: { + __typename: "Car", + make: "Audi", + model: "A4", + }, + }, + undefined, + { + car: { + __typename: "Car", + make: "Audi", + model: "RS8", + }, + }, + ]); + }); }); describe("Callbacks", () => { @@ -5690,7 +6017,10 @@ describe("useQuery Hook", () => { }, { interval: 1 } ); - expect(result.current.data).toEqual(carData); + const { vine, ...carDataWithoutVine } = carData.cars[0]; + expect(result.current.data).toEqual({ + cars: [carDataWithoutVine], + }); expect(result.current.error).toBeUndefined(); expect(consoleSpy.error).toHaveBeenCalled(); diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx new file mode 100644 index 00000000000..a7f2dd72f43 --- /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, + setupPaginatedCase, + setupSimpleCase, + 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"; +import { useBackgroundQuery } from "../useBackgroundQuery"; +import { useLoadableQuery } from "../useLoadableQuery"; +import { concatPagination } from "../../../utilities"; + +test("does not interfere with updates from useReadQuery", async () => { + const { query, mocks } = setupSimpleCase(); + + 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 } = setupSimpleCase(); + + 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 } = setupSimpleCase(); + + 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 } = setupSimpleCase(); + + 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 } = setupSimpleCase(); + + 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 } = setupPaginatedCase(); + + 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("properly uses `updateQuery` when calling `fetchMore`", async () => { + const { query, link } = setupPaginatedCase(); + + 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("properly uses cache field policies when calling `fetchMore` without `updateQuery`", async () => { + const { query, link } = setupPaginatedCase(); + + 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("paginates from queryRefs produced by useBackgroundQuery", async () => { + const { query, link } = setupPaginatedCase(); + + 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("paginates from queryRefs produced by useLoadableQuery", async () => { + const { query, link } = setupPaginatedCase(); + + 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("`fetchMore` works with startTransition", async () => { + const { query, link } = setupPaginatedCase(); + + 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", 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 } = setupPaginatedCase(); + + 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", 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: [ + { __typename: "Letter", letter: "E", position: 5 }, + { __typename: "Letter", letter: "F", position: 6 }, + ], + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }, + }); + } + + await expect(Profiler).not.toRerender(); +}); diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index e762e258fa8..6678763fb95 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -3642,8 +3642,7 @@ describe("useSuspenseQuery", () => { }); await waitFor(() => expect(renders.errorCount).toBe(1)); - - expect(client.getObservableQueries().size).toBe(0); + await waitFor(() => expect(client.getObservableQueries().size).toBe(0)); }); it('throws network errors when errorPolicy is set to "none"', async () => { diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 61d50665cac..78fc82c61f4 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -11,6 +11,13 @@ export type { UseSuspenseQueryResult } from "./useSuspenseQuery.js"; export { useSuspenseQuery } from "./useSuspenseQuery.js"; export type { UseBackgroundQueryResult } from "./useBackgroundQuery.js"; export { useBackgroundQuery } from "./useBackgroundQuery.js"; +export type { + LoadQueryFunction, + 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/internal/__use.ts b/src/react/hooks/internal/__use.ts index 2a49fab9e0c..b06760ff04e 100644 --- a/src/react/hooks/internal/__use.ts +++ b/src/react/hooks/internal/__use.ts @@ -1,5 +1,5 @@ import { wrapPromiseWithState } from "../../../utilities/index.js"; -import * as React from "react"; +import * as React from "rehackt"; type Use = (promise: Promise) => T; // Prevent webpack from complaining about our feature detection of the diff --git a/src/react/hooks/internal/index.ts b/src/react/hooks/internal/index.ts index db13ed03b39..71cfbdc1799 100644 --- a/src/react/hooks/internal/index.ts +++ b/src/react/hooks/internal/index.ts @@ -1,5 +1,6 @@ // These hooks are used internally and are not exported publicly by the library export { useDeepMemo } from "./useDeepMemo.js"; export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect.js"; +export { useRenderGuard } from "./useRenderGuard.js"; export { useLazyRef } from "./useLazyRef.js"; export { __use } from "./__use.js"; diff --git a/src/react/hooks/internal/useDeepMemo.ts b/src/react/hooks/internal/useDeepMemo.ts index 5a49115a49b..916e23a746d 100644 --- a/src/react/hooks/internal/useDeepMemo.ts +++ b/src/react/hooks/internal/useDeepMemo.ts @@ -1,5 +1,5 @@ import type { DependencyList } from "react"; -import * as React from "react"; +import * as React from "rehackt"; import { equal } from "@wry/equality"; export function useDeepMemo( diff --git a/src/react/hooks/internal/useIsomorphicLayoutEffect.ts b/src/react/hooks/internal/useIsomorphicLayoutEffect.ts index 1d45dc77c93..d5528cfde17 100644 --- a/src/react/hooks/internal/useIsomorphicLayoutEffect.ts +++ b/src/react/hooks/internal/useIsomorphicLayoutEffect.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import { canUseDOM } from "../../../utilities/index.js"; // use canUseDOM here instead of canUseLayoutEffect because we want to be able diff --git a/src/react/hooks/internal/useRenderGuard.ts b/src/react/hooks/internal/useRenderGuard.ts new file mode 100644 index 00000000000..98bb21a8ef1 --- /dev/null +++ b/src/react/hooks/internal/useRenderGuard.ts @@ -0,0 +1,22 @@ +import * as React from "rehackt"; + +function getRenderDispatcher() { + return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED + ?.ReactCurrentDispatcher?.current; +} + +let RenderDispatcher: unknown = null; + +/* +Relay does this too, so we hope this is safe. +https://github.com/facebook/relay/blob/8651fbca19adbfbb79af7a3bc40834d105fd7747/packages/react-relay/relay-hooks/loadQuery.js#L90-L98 +*/ +export function useRenderGuard() { + RenderDispatcher = getRenderDispatcher(); + + return React.useCallback(() => { + return ( + RenderDispatcher !== null && RenderDispatcher === getRenderDispatcher() + ); + }, []); +} diff --git a/src/react/hooks/useApolloClient.ts b/src/react/hooks/useApolloClient.ts index c9e81a8551c..8f4fdc80f0c 100644 --- a/src/react/hooks/useApolloClient.ts +++ b/src/react/hooks/useApolloClient.ts @@ -1,8 +1,23 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; import type { ApolloClient } from "../../core/index.js"; import { getApolloContext } from "../context/index.js"; +/** + * @example + * ```jsx + * import { useApolloClient } from '@apollo/client'; + * + * function SomeComponent() { + * const client = useApolloClient(); + * // `client` is now set to the `ApolloClient` instance being used by the + * // application (that was configured using something like `ApolloProvider`) + * } + * ``` + * + * @since 3.0.0 + * @returns The `ApolloClient` instance being used by the application. + */ export function useApolloClient( override?: ApolloClient ): ApolloClient { diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index dd55fdf9db9..ab9105d4243 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import type { DocumentNode, FetchMoreQueryOptions, @@ -7,16 +7,19 @@ import type { WatchQueryOptions, } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; -import { wrapQueryRef } from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; +import { + getSuspenseCache, + unwrapQueryRef, + updateWrappedQueryRef, + wrapQueryRef, +} from "../internal/index.js"; +import type { CacheKey, QueryReference } 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< @@ -47,7 +50,8 @@ export function useBackgroundQuery< DeepPartial | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? DeepPartial - : TData + : TData, + TVariables > | (TOptions["skip"] extends boolean ? undefined : never) ), @@ -64,7 +68,7 @@ export function useBackgroundQuery< errorPolicy: "ignore" | "all"; } ): [ - QueryReference | undefined>, + QueryReference | undefined, TVariables>, UseBackgroundQueryResult, ]; @@ -77,7 +81,7 @@ export function useBackgroundQuery< errorPolicy: "ignore" | "all"; } ): [ - QueryReference, + QueryReference, UseBackgroundQueryResult, ]; @@ -91,7 +95,7 @@ export function useBackgroundQuery< returnPartialData: true; } ): [ - QueryReference> | undefined, + QueryReference, TVariables> | undefined, UseBackgroundQueryResult, ]; @@ -104,7 +108,7 @@ export function useBackgroundQuery< returnPartialData: true; } ): [ - QueryReference>, + QueryReference, TVariables>, UseBackgroundQueryResult, ]; @@ -117,7 +121,7 @@ export function useBackgroundQuery< skip: boolean; } ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ]; @@ -127,7 +131,10 @@ export function useBackgroundQuery< >( query: DocumentNode | TypedDocumentNode, options?: BackgroundQueryHookOptionsNoInfer -): [QueryReference, UseBackgroundQueryResult]; +): [ + QueryReference, + UseBackgroundQueryResult, +]; export function useBackgroundQuery< TData = unknown, @@ -148,7 +155,7 @@ export function useBackgroundQuery< returnPartialData: true; }) ): [ - QueryReference> | undefined, + QueryReference, TVariables> | undefined, UseBackgroundQueryResult, ]; @@ -159,7 +166,7 @@ export function useBackgroundQuery< query: DocumentNode | TypedDocumentNode, options?: SkipToken | BackgroundQueryHookOptionsNoInfer ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ]; @@ -173,7 +180,7 @@ export function useBackgroundQuery< Partial>) | BackgroundQueryHookOptionsNoInfer = Object.create(null) ): [ - QueryReference | undefined, + QueryReference | undefined, UseBackgroundQueryResult, ] { const client = useApolloClient(options.client); @@ -201,24 +208,22 @@ export function useBackgroundQuery< client.watchQuery(watchQueryOptions as WatchQueryOptions) ); - const [promiseCache, setPromiseCache] = React.useState( - () => new Map([[queryRef.key, queryRef.promise]]) + const [wrappedQueryRef, setWrappedQueryRef] = React.useState( + wrapQueryRef(queryRef) ); - + if (unwrapQueryRef(wrappedQueryRef) !== queryRef) { + setWrappedQueryRef(wrapQueryRef(queryRef)); + } if (queryRef.didChangeOptions(watchQueryOptions)) { const promise = queryRef.applyOptions(watchQueryOptions); - promiseCache.set(queryRef.key, promise); + updateWrappedQueryRef(wrappedQueryRef, promise); } - React.useEffect(() => queryRef.retain(), [queryRef]); - const fetchMore: FetchMoreFunction = React.useCallback( (options) => { const promise = queryRef.fetchMore(options as FetchMoreQueryOptions); - setPromiseCache((promiseCache) => - new Map(promiseCache).set(queryRef.key, queryRef.promise) - ); + setWrappedQueryRef(wrapQueryRef(queryRef)); return promise; }, @@ -229,22 +234,13 @@ export function useBackgroundQuery< (variables) => { const promise = queryRef.refetch(variables); - setPromiseCache((promiseCache) => - new Map(promiseCache).set(queryRef.key, queryRef.promise) - ); + setWrappedQueryRef(wrapQueryRef(queryRef)); return promise; }, [queryRef] ); - queryRef.promiseCache = promiseCache; - - const wrappedQueryRef = React.useMemo( - () => wrapQueryRef(queryRef), - [queryRef] - ); - return [ didFetchResult.current ? wrappedQueryRef : void 0, { fetchMore, refetch }, diff --git a/src/react/hooks/useFragment.ts b/src/react/hooks/useFragment.ts index b686689fc79..f92bc8f42f1 100644 --- a/src/react/hooks/useFragment.ts +++ b/src/react/hooks/useFragment.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import { equal } from "@wry/equality"; import type { DeepPartial } from "../../utilities/index.js"; diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index 544bd08fb0d..6c58c86e3ce 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -1,6 +1,6 @@ import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; -import * as React from "react"; +import * as React from "rehackt"; import type { OperationVariables } from "../../core/index.js"; import { mergeOptions } from "../../utilities/index.js"; @@ -25,6 +25,41 @@ const EAGER_METHODS = [ "subscribeToMore", ] as const; +/** + * A hook for imperatively executing queries in an Apollo application, e.g. in response to user interaction. + * + * > Refer to the [Queries - Manual execution with useLazyQuery](https://www.apollographql.com/docs/react/data/queries#manual-execution-with-uselazyquery) section for a more in-depth overview of `useLazyQuery`. + * + * @example + * ```jsx + * import { gql, useLazyQuery } from "@apollo/client"; + * + * const GET_GREETING = gql` + * query GetGreeting($language: String!) { + * greeting(language: $language) { + * message + * } + * } + * `; + * + * function Hello() { + * const [loadGreeting, { called, loading, data }] = useLazyQuery( + * GET_GREETING, + * { variables: { language: "english" } } + * ); + * if (called && loading) return

Loading ...

+ * if (!called) { + * return + * } + * return

Hello {data.greeting.message}!

; + * } + * ``` + * @since 3.0.0 + * + * @param query - A GraphQL query document parsed into an AST by `gql`. + * @param options - Default options to control how the query is executed. + * @returns A tuple in the form of `[execute, result]` + */ export function useLazyQuery< TData = any, TVariables extends OperationVariables = OperationVariables, diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts new file mode 100644 index 00000000000..7c0c0cca4e6 --- /dev/null +++ b/src/react/hooks/useLoadableQuery.ts @@ -0,0 +1,253 @@ +import * as React from "rehackt"; +import type { + DocumentNode, + FetchMoreQueryOptions, + OperationVariables, + TypedDocumentNode, + WatchQueryOptions, +} from "../../core/index.js"; +import { useApolloClient } from "./useApolloClient.js"; +import { + getSuspenseCache, + unwrapQueryRef, + updateWrappedQueryRef, + wrapQueryRef, +} from "../internal/index.js"; +import type { CacheKey, QueryReference } from "../internal/index.js"; +import type { LoadableQueryHookOptions } from "../types/types.js"; +import { __use, useRenderGuard } from "./internal/index.js"; +import { useWatchQueryOptions } from "./useSuspenseQuery.js"; +import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; +import { canonicalStringify } from "../../cache/index.js"; +import type { + DeepPartial, + OnlyRequiredProperties, +} from "../../utilities/index.js"; +import { invariant } from "../../utilities/globals/index.js"; + +export type LoadQueryFunction = ( + // Use variadic args to handle cases where TVariables is type `never`, in + // which case we don't want to allow a variables argument. In other + // words, we don't want to allow variables to be passed as an argument to this + // function if the query does not expect variables in the document. + ...args: [TVariables] extends [never] ? [] + : {} extends OnlyRequiredProperties ? [variables?: TVariables] + : [variables: TVariables] +) => void; + +type ResetFunction = () => void; + +export type UseLoadableQueryResult< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +> = [ + loadQuery: LoadQueryFunction, + queryRef: QueryReference | null, + { + /** {@inheritDoc @apollo/client!QueryResultDocumentation#fetchMore:member} */ + fetchMore: FetchMoreFunction; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#refetch:member} */ + refetch: RefetchFunction; + /** + * A function that resets the `queryRef` back to `null`. + */ + reset: ResetFunction; + }, +]; + +export function useLoadableQuery< + TData, + TVariables extends OperationVariables, + TOptions extends LoadableQueryHookOptions, +>( + query: DocumentNode | TypedDocumentNode, + options?: LoadableQueryHookOptions & TOptions +): UseLoadableQueryResult< + TOptions["errorPolicy"] extends "ignore" | "all" ? + TOptions["returnPartialData"] extends true ? + DeepPartial | undefined + : TData | undefined + : TOptions["returnPartialData"] extends true ? DeepPartial + : TData, + TVariables +>; + +export function useLoadableQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: LoadableQueryHookOptions & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + } +): UseLoadableQueryResult | undefined, TVariables>; + +export function useLoadableQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: LoadableQueryHookOptions & { + errorPolicy: "ignore" | "all"; + } +): UseLoadableQueryResult; + +export function useLoadableQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: LoadableQueryHookOptions & { + returnPartialData: true; + } +): UseLoadableQueryResult, TVariables>; + +/** + * A hook for imperatively loading a query, such as responding to a user + * interaction. + * + * > Refer to the [Suspense - Fetching in response to user interaction](https://www.apollographql.com/docs/react/data/suspense#fetching-in-response-to-user-interaction) section for a more in-depth overview of `useLoadableQuery`. + * + * @example + * ```jsx + * import { gql, useLoadableQuery } from "@apollo/client"; + * + * const GET_GREETING = gql` + * query GetGreeting($language: String!) { + * greeting(language: $language) { + * message + * } + * } + * `; + * + * function App() { + * const [loadGreeting, queryRef] = useLoadableQuery(GET_GREETING); + * + * return ( + * <> + * + * Loading...}> + * {queryRef && } + * + * + * ); + * } + * + * function Hello({ queryRef }) { + * const { data } = useReadQuery(queryRef); + * + * return
{data.greeting.message}
; + * } + * ``` + * + * @since 3.9.0 + * @param query - A GraphQL query document parsed into an AST by `gql`. + * @param options - Options to control how the query is executed. + * @returns A tuple in the form of `[loadQuery, queryRef, handlers]` + */ +export function useLoadableQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options?: LoadableQueryHookOptions +): UseLoadableQueryResult; + +export function useLoadableQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + query: DocumentNode | TypedDocumentNode, + options: LoadableQueryHookOptions = Object.create(null) +): UseLoadableQueryResult { + const client = useApolloClient(options.client); + const suspenseCache = getSuspenseCache(client); + const watchQueryOptions = useWatchQueryOptions({ client, query, options }); + const { queryKey = [] } = options; + + const [queryRef, setQueryRef] = React.useState | null>(null); + + const internalQueryRef = queryRef && unwrapQueryRef(queryRef); + + if (queryRef && internalQueryRef?.didChangeOptions(watchQueryOptions)) { + const promise = internalQueryRef.applyOptions(watchQueryOptions); + updateWrappedQueryRef(queryRef, promise); + } + + const calledDuringRender = useRenderGuard(); + + const fetchMore: FetchMoreFunction = React.useCallback( + (options) => { + if (!internalQueryRef) { + throw new Error( + "The query has not been loaded. Please load the query." + ); + } + + const promise = internalQueryRef.fetchMore( + options as FetchMoreQueryOptions + ); + + setQueryRef(wrapQueryRef(internalQueryRef)); + + return promise; + }, + [internalQueryRef] + ); + + const refetch: RefetchFunction = React.useCallback( + (options) => { + if (!internalQueryRef) { + throw new Error( + "The query has not been loaded. Please load the query." + ); + } + + const promise = internalQueryRef.refetch(options); + + setQueryRef(wrapQueryRef(internalQueryRef)); + + return promise; + }, + [internalQueryRef] + ); + + const loadQuery: LoadQueryFunction = React.useCallback( + (...args) => { + invariant( + !calledDuringRender(), + "useLoadableQuery: 'loadQuery' should not be called during render. To start a query during render, use the 'useBackgroundQuery' hook." + ); + + const [variables] = args; + + const cacheKey: CacheKey = [ + query, + canonicalStringify(variables), + ...([] as any[]).concat(queryKey), + ]; + + const queryRef = suspenseCache.getQueryRef(cacheKey, () => + client.watchQuery({ + ...watchQueryOptions, + variables, + } as WatchQueryOptions) + ); + + setQueryRef(wrapQueryRef(queryRef)); + }, + [query, queryKey, suspenseCache, watchQueryOptions, calledDuringRender] + ); + + const reset: ResetFunction = React.useCallback(() => { + setQueryRef(null); + }, [queryRef]); + + return [loadQuery, queryRef, { fetchMore, refetch, reset }]; +} diff --git a/src/react/hooks/useMutation.ts b/src/react/hooks/useMutation.ts index 643daa60e2d..79825f91524 100644 --- a/src/react/hooks/useMutation.ts +++ b/src/react/hooks/useMutation.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import type { @@ -21,6 +21,53 @@ import { DocumentType, verifyDocumentType } from "../parser/index.js"; import { ApolloError } from "../../errors/index.js"; import { useApolloClient } from "./useApolloClient.js"; +/** + * + * + * > Refer to the [Mutations](https://www.apollographql.com/docs/react/data/mutations/) section for a more in-depth overview of `useMutation`. + * + * @example + * ```jsx + * import { gql, useMutation } from '@apollo/client'; + * + * const ADD_TODO = gql` + * mutation AddTodo($type: String!) { + * addTodo(type: $type) { + * id + * type + * } + * } + * `; + * + * function AddTodo() { + * let input; + * const [addTodo, { data }] = useMutation(ADD_TODO); + * + * return ( + *
+ *
{ + * e.preventDefault(); + * addTodo({ variables: { type: input.value } }); + * input.value = ''; + * }} + * > + * { + * input = node; + * }} + * /> + * + *
+ *
+ * ); + * } + * ``` + * @since 3.0.0 + * @param mutation - A GraphQL mutation document parsed into an AST by `gql`. + * @param options - Options to control how the mutation is executed. + * @returns A tuple in the form of `[mutate, result]` + */ export function useMutation< TData = any, TVariables = OperationVariables, diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index 696d826f0d0..4bc596ed371 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -1,6 +1,6 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import { equal } from "@wry/equality"; @@ -41,6 +41,40 @@ const { prototype: { hasOwnProperty }, } = Object; +/** + * A hook for executing queries in an Apollo application. + * + * To run a query within a React component, call `useQuery` and pass it a GraphQL query document. + * + * When your component renders, `useQuery` returns an object from Apollo Client that contains `loading`, `error`, and `data` properties you can use to render your UI. + * + * > Refer to the [Queries](https://www.apollographql.com/docs/react/data/queries) section for a more in-depth overview of `useQuery`. + * + * @example + * ```jsx + * import { gql, useQuery } from '@apollo/client'; + * + * const GET_GREETING = gql` + * query GetGreeting($language: String!) { + * greeting(language: $language) { + * message + * } + * } + * `; + * + * function Hello() { + * const { loading, error, data } = useQuery(GET_GREETING, { + * variables: { language: 'english' }, + * }); + * if (loading) return

Loading ...

; + * return

Hello {data.greeting.message}!

; + * } + * ``` + * @since 3.0.0 + * @param query - A GraphQL query document parsed into an AST by `gql`. + * @param options - Options to control how the query is executed. + * @returns Query result object + */ export function useQuery< TData = any, TVariables extends OperationVariables = OperationVariables, diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts new file mode 100644 index 00000000000..b0422afa678 --- /dev/null +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -0,0 +1,87 @@ +import * as React from "rehackt"; +import { + getWrappedPromise, + unwrapQueryRef, + updateWrappedQueryRef, + wrapQueryRef, +} from "../internal/index.js"; +import type { 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, +> { + /** {@inheritDoc @apollo/client!ObservableQuery#refetch:member(1)} */ + refetch: RefetchFunction; + /** {@inheritDoc @apollo/client!ObservableQuery#fetchMore:member(1)} */ + fetchMore: FetchMoreFunction; +} + +/** + * A React hook that returns a `refetch` and `fetchMore` function for a given + * `queryRef`. + * + * This is useful to get access to handlers for a `queryRef` that was created by + * `createQueryPreloader` or when the handlers for a `queryRef` produced in + * a different component are inaccessible. + * + * @example + * ```tsx + * const MyComponent({ queryRef }) { + * const { refetch, fetchMore } = useQueryRefHandlers(queryRef); + * + * // ... + * } + * ``` + * @since 3.9.0 + * @param queryRef - A `QueryReference` returned from `useBackgroundQuery`, `useLoadableQuery`, or `createQueryPreloader`. + */ +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, getWrappedPromise(wrappedQueryRef)); + } + + 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/useReactiveVar.ts b/src/react/hooks/useReactiveVar.ts index 2d14c12cd63..ed7ac2379d3 100644 --- a/src/react/hooks/useReactiveVar.ts +++ b/src/react/hooks/useReactiveVar.ts @@ -1,7 +1,24 @@ -import * as React from "react"; +import * as React from "rehackt"; import type { ReactiveVar } from "../../core/index.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; +/** + * Reads the value of a [reactive variable](https://www.apollographql.com/docs/react/local-state/reactive-variables/) and re-renders the containing component whenever that variable's value changes. This enables a reactive variable to trigger changes _without_ relying on the `useQuery` hook. + * + * @example + * ```jsx + * import { makeVar, useReactiveVar } from "@apollo/client"; + * export const cartItemsVar = makeVar([]); + * + * export function Cart() { + * const cartItems = useReactiveVar(cartItemsVar); + * // ... + * } + * ``` + * @since 3.2.0 + * @param rv - A reactive variable. + * @returns The current value of the reactive variable. + */ export function useReactiveVar(rv: ReactiveVar): T { return useSyncExternalStore( React.useCallback( diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index e6a97e1446f..3f110b26164 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -1,9 +1,12 @@ -import * as React from "react"; -import { unwrapQueryRef } from "../cache/QueryReference.js"; -import type { QueryReference } from "../cache/QueryReference.js"; +import * as React from "rehackt"; +import { + getWrappedPromise, + 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 { invariant } from "../../utilities/globals/index.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import type { ApolloError } from "../../errors/index.js"; import type { NetworkStatus } from "../../core/index.js"; @@ -36,32 +39,35 @@ export interface UseReadQueryResult { export function useReadQuery( queryRef: QueryReference ): UseReadQueryResult { - const internalQueryRef = unwrapQueryRef(queryRef); - invariant( - internalQueryRef.promiseCache, - "It appears that `useReadQuery` was used outside of `useBackgroundQuery`. " + - "`useReadQuery` is only supported for use with `useBackgroundQuery`. " + - "Please ensure you are passing the `queryRef` returned from `useBackgroundQuery`." + const internalQueryRef = React.useMemo( + () => unwrapQueryRef(queryRef), + [queryRef] ); - const { promiseCache, key } = internalQueryRef; + const getPromise = React.useCallback( + () => getWrappedPromise(queryRef), + [queryRef] + ); - if (!promiseCache.has(key)) { - promiseCache.set(key, internalQueryRef.promise); + if (internalQueryRef.disposed) { + internalQueryRef.reinitialize(); + updateWrappedQueryRef(queryRef, internalQueryRef.promise); } + React.useEffect(() => internalQueryRef.retain(), [internalQueryRef]); + const promise = useSyncExternalStore( React.useCallback( (forceUpdate) => { return internalQueryRef.listen((promise) => { - internalQueryRef.promiseCache!.set(internalQueryRef.key, promise); + updateWrappedQueryRef(queryRef, promise); forceUpdate(); }); }, [internalQueryRef] ), - () => promiseCache.get(key)!, - () => promiseCache.get(key)! + getPromise, + getPromise ); const result = __use(promise); diff --git a/src/react/hooks/useSubscription.ts b/src/react/hooks/useSubscription.ts index 024f4aa4a2a..366ebfe97f4 100644 --- a/src/react/hooks/useSubscription.ts +++ b/src/react/hooks/useSubscription.ts @@ -1,5 +1,5 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { equal } from "@wry/equality"; @@ -12,7 +12,91 @@ import type { } from "../types/types.js"; import type { OperationVariables } from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; - +/** + * > Refer to the [Subscriptions](https://www.apollographql.com/docs/react/data/subscriptions/) section for a more in-depth overview of `useSubscription`. + * + * @example + * ```jsx + * const COMMENTS_SUBSCRIPTION = gql` + * subscription OnCommentAdded($repoFullName: String!) { + * commentAdded(repoFullName: $repoFullName) { + * id + * content + * } + * } + * `; + * + * function DontReadTheComments({ repoFullName }) { + * const { + * data: { commentAdded }, + * loading, + * } = useSubscription(COMMENTS_SUBSCRIPTION, { variables: { repoFullName } }); + * return

New comment: {!loading && commentAdded.content}

; + * } + * ``` + * @remarks + * #### Subscriptions and React 18 Automatic Batching + * + * With React 18's [automatic batching](https://react.dev/blog/2022/03/29/react-v18#new-feature-automatic-batching), multiple state updates may be grouped into a single re-render for better performance. + * + * If your subscription API sends multiple messages at the same time or in very fast succession (within fractions of a millisecond), it is likely that only the last message received in that narrow time frame will result in a re-render. + * + * Consider the following component: + * + * ```jsx + * export function Subscriptions() { + * const { data, error, loading } = useSubscription(query); + * const [accumulatedData, setAccumulatedData] = useState([]); + * + * useEffect(() => { + * setAccumulatedData((prev) => [...prev, data]); + * }, [data]); + * + * return ( + * <> + * {loading &&

Loading...

} + * {JSON.stringify(accumulatedData, undefined, 2)} + * + * ); + * } + * ``` + * + * If your subscription back-end emits two messages with the same timestamp, only the last message received by Apollo Client will be rendered. This is because React 18 will batch these two state updates into a single re-render. + * + * Since the component above is using `useEffect` to push `data` into a piece of local state on each `Subscriptions` re-render, the first message will never be added to the `accumulatedData` array since its render was skipped. + * + * Instead of using `useEffect` here, we can re-write this component to use the `onData` callback function accepted in `useSubscription`'s `options` object: + * + * ```jsx + * export function Subscriptions() { + * const [accumulatedData, setAccumulatedData] = useState([]); + * const { data, error, loading } = useSubscription( + * query, + * { + * onData({ data }) { + * setAccumulatedData((prev) => [...prev, data]) + * } + * } + * ); + * + * return ( + * <> + * {loading &&

Loading...

} + * {JSON.stringify(accumulatedData, undefined, 2)} + * + * ); + * } + * ``` + * + * > ⚠️ **Note:** The `useSubscription` option `onData` is available in Apollo Client >= 3.7. In previous versions, the equivalent option is named `onSubscriptionData`. + * + * Now, the first message will be added to the `accumulatedData` array since `onData` is called _before_ the component re-renders. React 18 automatic batching is still in effect and results in a single re-render, but with `onData` we can guarantee each message received after the component mounts is added to `accumulatedData`. + * + * @since 3.0.0 + * @param subscription - A GraphQL subscription document parsed into an AST by `gql`. + * @param options - Options to control how the subscription is executed. + * @returns Query result object + */ export function useSubscription< TData = any, TVariables extends OperationVariables = OperationVariables, diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index 0f957473558..3a69e175ed5 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -1,4 +1,4 @@ -import * as React from "react"; +import * as React from "rehackt"; import { invariant } from "../../utilities/globals/index.js"; import type { ApolloClient, @@ -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 } from "../cache/types.js"; +import type { CacheKey, QueryKey } from "../internal/index.js"; export interface UseSuspenseQueryResult< TData = unknown, @@ -195,29 +195,26 @@ export function useSuspenseQuery< client.watchQuery(watchQueryOptions) ); - const [promiseCache, setPromiseCache] = React.useState( - () => new Map([[queryRef.key, queryRef.promise]]) - ); - - let promise = promiseCache.get(queryRef.key); + let [current, setPromise] = React.useState< + [QueryKey, Promise>] + >([queryRef.key, queryRef.promise]); - if (queryRef.didChangeOptions(watchQueryOptions)) { - promise = queryRef.applyOptions(watchQueryOptions); - promiseCache.set(queryRef.key, promise); + // This saves us a re-execution of the render function when a variable changed. + if (current[0] !== queryRef.key) { + current[0] = queryRef.key; + current[1] = queryRef.promise; } + let promise = current[1]; - if (!promise) { - promise = queryRef.promise; - promiseCache.set(queryRef.key, promise); + if (queryRef.didChangeOptions(watchQueryOptions)) { + current[1] = promise = queryRef.applyOptions(watchQueryOptions); } React.useEffect(() => { const dispose = queryRef.retain(); const removeListener = queryRef.listen((promise) => { - setPromiseCache((promiseCache) => - new Map(promiseCache).set(queryRef.key, promise) - ); + setPromise([queryRef.key, promise]); }); return () => { @@ -238,14 +235,10 @@ export function useSuspenseQuery< }, [queryRef.result]); const result = fetchPolicy === "standby" ? skipResult : __use(promise); - const fetchMore = React.useCallback( ((options) => { const promise = queryRef.fetchMore(options); - - setPromiseCache((previousPromiseCache) => - new Map(previousPromiseCache).set(queryRef.key, queryRef.promise) - ); + setPromise([queryRef.key, queryRef.promise]); return promise; }) satisfies FetchMoreFunction< @@ -258,10 +251,7 @@ export function useSuspenseQuery< const refetch: RefetchFunction = React.useCallback( (variables) => { const promise = queryRef.refetch(variables); - - setPromiseCache((previousPromiseCache) => - new Map(previousPromiseCache).set(queryRef.key, queryRef.promise) - ); + setPromise([queryRef.key, queryRef.promise]); return promise; }, diff --git a/src/react/hooks/useSyncExternalStore.ts b/src/react/hooks/useSyncExternalStore.ts index 0ae0a84d793..adf4d059f7f 100644 --- a/src/react/hooks/useSyncExternalStore.ts +++ b/src/react/hooks/useSyncExternalStore.ts @@ -1,5 +1,5 @@ import { invariant } from "../../utilities/globals/index.js"; -import * as React from "react"; +import * as React from "rehackt"; import { canUseLayoutEffect } from "../../utilities/index.js"; diff --git a/src/react/index.ts b/src/react/index.ts index 784046d950b..13f1103e41f 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 type { + PreloadQueryOptions, + PreloadQueryFetchPolicy, + PreloadQueryFunction, +} from "./query-preloader/createQueryPreloader.js"; +export { createQueryPreloader } from "./query-preloader/createQueryPreloader.js"; + export * from "./types/types.js"; diff --git a/src/react/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts similarity index 55% rename from src/react/cache/QueryReference.ts rename to src/react/internal/cache/QueryReference.ts index a238fc1827f..dc26adf541c 100644 --- a/src/react/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -5,42 +5,116 @@ import type { ObservableQuery, OperationVariables, WatchQueryOptions, -} from "../../core/index.js"; -import { isNetworkRequestSettled } from "../../core/index.js"; -import type { ObservableSubscription } from "../../utilities/index.js"; +} from "../../../core/index.js"; +import type { + ObservableSubscription, + PromiseWithState, +} from "../../../utilities/index.js"; import { createFulfilledPromise, createRejectedPromise, -} from "../../utilities/index.js"; -import type { CacheKey } from "./types.js"; -import type { useBackgroundQuery, useReadQuery } from "../hooks/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"; + +type QueryRefPromise = PromiseWithState>; -type Listener = (promise: Promise>) => void; +type Listener = (promise: QueryRefPromise) => void; type FetchMoreOptions = Parameters< ObservableQuery["fetchMore"] >[0]; const QUERY_REFERENCE_SYMBOL: unique symbol = Symbol(); +const PROMISE_SYMBOL: unique symbol = Symbol(); + /** * A `QueryReference` is an opaque object returned by {@link useBackgroundQuery}. * A child component reading the `QueryReference` via {@link useReadQuery} will * suspend until the promise resolves. */ -export interface QueryReference { - [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; +export interface QueryReference { + /** @internal */ + readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; + /** @internal */ + [PROMISE_SYMBOL]: QueryRefPromise; + /** + * A function that returns a promise that resolves when the query has finished + * loading. The promise resolves with the `QueryReference` itself. + * + * @remarks + * This method is useful for preloading queries in data loading routers, such + * as [React Router](https://reactrouter.com/en/main) or [TanStack Router](https://tanstack.com/router), + * to prevent routes from transitioning until the query has finished loading. + * `data` is not exposed on the promise to discourage using the data in + * `loader` functions and exposing it to your route components. Instead, we + * prefer you rely on `useReadQuery` to access the data to ensure your + * component can rerender with cache updates. If you need to access raw query + * data, use `client.query()` directly. + * + * @example + * Here's an example using React Router's `loader` function: + * ```ts + * import { createQueryPreloader } from "@apollo/client"; + * + * const preloadQuery = createQueryPreloader(client); + * + * export async function loader() { + * const queryRef = preloadQuery(GET_DOGS_QUERY); + * + * return queryRef.toPromise(); + * } + * + * export function RouteComponent() { + * const queryRef = useLoaderData(); + * const { data } = useReadQuery(queryRef); + * + * // ... + * } + * ``` + * + * @alpha + */ + toPromise(): Promise>; } interface InternalQueryReferenceOptions { - key: CacheKey; onDispose?: () => void; autoDisposeTimeoutMs?: number; } -export function wrapQueryRef( +export function wrapQueryRef( internalQueryRef: InternalQueryReference -): QueryReference { - return { [QUERY_REFERENCE_SYMBOL]: internalQueryRef }; +) { + const ref: QueryReference = { + toPromise() { + // We avoid resolving this promise with the query data because we want to + // discourage using the server data directly from the queryRef. Instead, + // the data should be accessed through `useReadQuery`. When the server + // data is needed, its better to use `client.query()` directly. + // + // Here we resolve with the ref itself to make using this in React Router + // or TanStack Router `loader` functions a bit more ergonomic e.g. + // + // function loader() { + // return { queryRef: await preloadQuery(query).toPromise() } + // } + return getWrappedPromise(ref).then(() => ref); + }, + [QUERY_REFERENCE_SYMBOL]: internalQueryRef, + [PROMISE_SYMBOL]: internalQueryRef.promise, + }; + + return ref; +} + +export function getWrappedPromise(queryRef: QueryReference) { + const internalQueryRef = unwrapQueryRef(queryRef); + + return internalQueryRef.promise.status === "fulfilled" ? + internalQueryRef.promise + : queryRef[PROMISE_SYMBOL]; } export function unwrapQueryRef( @@ -49,6 +123,13 @@ export function unwrapQueryRef( return queryRef[QUERY_REFERENCE_SYMBOL]; } +export function updateWrappedQueryRef( + queryRef: QueryReference, + promise: QueryRefPromise +) { + queryRef[PROMISE_SYMBOL] = promise; +} + const OBSERVED_CHANGED_OPTIONS = [ "canonizeResults", "context", @@ -64,17 +145,15 @@ type ObservedOptions = Pick< >; export class InternalQueryReference { - public result: ApolloQueryResult; - public readonly key: CacheKey; + public result!: ApolloQueryResult; + public readonly key: QueryKey = {}; public readonly observable: ObservableQuery; - public promiseCache?: Map>>; - public promise: Promise>; + public promise!: QueryRefPromise; - private subscription: ObservableSubscription; + private subscription!: ObservableSubscription; private listeners = new Set>(); private autoDisposeTimeoutId?: NodeJS.Timeout; - private status: "idle" | "loading" = "loading"; private resolve: ((result: ApolloQueryResult) => void) | undefined; private reject: ((error: unknown) => void) | undefined; @@ -82,42 +161,20 @@ export class InternalQueryReference { private references = 0; constructor( - observable: ObservableQuery, + observable: ObservableQuery, options: InternalQueryReferenceOptions ) { this.handleNext = this.handleNext.bind(this); this.handleError = this.handleError.bind(this); this.dispose = this.dispose.bind(this); this.observable = observable; - // Don't save this result as last result to prevent delivery of last result - // when first subscribing - this.result = observable.getCurrentResult(false); - this.key = options.key; if (options.onDispose) { this.onDispose = options.onDispose; } - if ( - isNetworkRequestSettled(this.result.networkStatus) || - (this.result.data && - (!this.result.partial || this.watchQueryOptions.returnPartialData)) - ) { - this.promise = createFulfilledPromise(this.result); - this.status = "idle"; - } else { - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); - } - - this.subscription = observable - .filter(({ data }) => !equal(data, {})) - .subscribe({ - next: this.handleNext, - error: this.handleError, - }); + this.setResult(); + this.subscribeToQuery(); // Start a timer that will automatically dispose of the query if the // suspended resource does not use this queryRef in the given time. This @@ -138,10 +195,40 @@ export class InternalQueryReference { this.promise.then(startDisposeTimer, startDisposeTimer); } + get disposed() { + return this.subscription.closed; + } + get watchQueryOptions() { return this.observable.options; } + reinitialize() { + const { observable } = this; + + const originalFetchPolicy = this.watchQueryOptions.fetchPolicy; + + try { + if (originalFetchPolicy !== "no-cache") { + observable.resetLastResults(); + observable.silentSetOptions({ fetchPolicy: "cache-first" }); + } else { + observable.silentSetOptions({ fetchPolicy: "standby" }); + } + + this.subscribeToQuery(); + + if (originalFetchPolicy === "no-cache") { + return; + } + + observable.resetDiff(); + this.setResult(); + } finally { + observable.silentSetOptions({ fetchPolicy: originalFetchPolicy }); + } + } + retain() { this.references++; clearTimeout(this.autoDisposeTimeoutId); @@ -222,19 +309,18 @@ export class InternalQueryReference { } private handleNext(result: ApolloQueryResult) { - switch (this.status) { - case "loading": { + switch (this.promise.status) { + case "pending": { // Maintain the last successful `data` value if the next result does not // have one. if (result.data === void 0) { result.data = this.result.data; } - this.status = "idle"; this.result = result; this.resolve?.(result); break; } - case "idle": { + default: { // This occurs when switching to a result that is fully cached when this // class is instantiated. ObservableQuery will run reobserve when // subscribing, which delivers a result from the cache. @@ -266,31 +352,24 @@ export class InternalQueryReference { this.handleError ); - switch (this.status) { - case "loading": { - this.status = "idle"; + switch (this.promise.status) { + case "pending": { this.reject?.(error); break; } - case "idle": { - this.promise = createRejectedPromise(error); + default: { + this.promise = createRejectedPromise>(error); this.deliver(this.promise); } } } - private deliver(promise: Promise>) { + private deliver(promise: QueryRefPromise) { this.listeners.forEach((listener) => listener(promise)); } private initiateFetch(returnedPromise: Promise>) { - this.status = "loading"; - - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); - + this.promise = this.createPendingPromise(); this.promise.catch(() => {}); // If the data returned from the fetch is deeply equal to the data already @@ -300,8 +379,7 @@ export class InternalQueryReference { // promise is resolved correctly. returnedPromise .then((result) => { - if (this.status === "loading") { - this.status = "idle"; + if (this.promise.status === "pending") { this.result = result; this.resolve?.(result); } @@ -310,4 +388,40 @@ export class InternalQueryReference { return returnedPromise; } + + private subscribeToQuery() { + this.subscription = this.observable + .filter( + (result) => !equal(result.data, {}) && !equal(result, this.result) + ) + .subscribe(this.handleNext, this.handleError); + } + + private setResult() { + // Don't save this result as last result to prevent delivery of last result + // when first subscribing + const result = this.observable.getCurrentResult(false); + + if (equal(result, this.result)) { + return; + } + + this.result = result; + this.promise = + ( + result.data && + (!result.partial || this.watchQueryOptions.returnPartialData) + ) ? + createFulfilledPromise(result) + : this.createPendingPromise(); + } + + private createPendingPromise() { + return wrapPromiseWithState( + new Promise>((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }) + ); + } } diff --git a/src/react/cache/SuspenseCache.ts b/src/react/internal/cache/SuspenseCache.ts similarity index 91% rename from src/react/cache/SuspenseCache.ts rename to src/react/internal/cache/SuspenseCache.ts index af883c0a52a..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"; @@ -38,7 +38,6 @@ export class SuspenseCache { if (!ref.current) { ref.current = new InternalQueryReference(createObservable(), { - key: cacheKey, autoDisposeTimeoutMs: this.options.autoDisposeTimeoutMs, onDispose: () => { delete ref.current; 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..7e015109333 --- /dev/null +++ b/src/react/internal/cache/__tests__/QueryReference.test.ts @@ -0,0 +1,27 @@ +import { + ApolloClient, + ApolloLink, + InMemoryCache, + Observable, +} from "../../../../core"; +import { setupSimpleCase } from "../../../../testing/internal"; +import { InternalQueryReference } from "../QueryReference"; + +test("kicks off request immediately when created", async () => { + const { query } = setupSimpleCase(); + let fetchCount = 0; + + const client = new ApolloClient({ + cache: new InMemoryCache(), + link: new ApolloLink((operation) => { + fetchCount++; + return Observable.of({ data: { greeting: "Hello" } }); + }), + }); + + const observable = client.watchQuery({ query }); + + expect(fetchCount).toBe(0); + new InternalQueryReference(observable, {}); + expect(fetchCount).toBe(1); +}); diff --git a/src/react/cache/getSuspenseCache.ts b/src/react/internal/cache/getSuspenseCache.ts similarity index 75% rename from src/react/cache/getSuspenseCache.ts rename to src/react/internal/cache/getSuspenseCache.ts index 93fbc72b98a..d9547cdc995 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 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; diff --git a/src/react/cache/types.ts b/src/react/internal/cache/types.ts similarity index 73% rename from src/react/cache/types.ts rename to src/react/internal/cache/types.ts index e9e0ba8b826..40f3c4cc8fc 100644 --- a/src/react/cache/types.ts +++ b/src/react/internal/cache/types.ts @@ -5,3 +5,7 @@ export type CacheKey = [ stringifiedVariables: string, ...queryKey: any[], ]; + +export interface QueryKey { + __queryKey?: string; +} diff --git a/src/react/internal/index.ts b/src/react/internal/index.ts new file mode 100644 index 00000000000..cbcab8f0209 --- /dev/null +++ b/src/react/internal/index.ts @@ -0,0 +1,11 @@ +export { getSuspenseCache } from "./cache/getSuspenseCache.js"; +export type { CacheKey, QueryKey } from "./cache/types.js"; +export type { QueryReference } from "./cache/QueryReference.js"; +export { + InternalQueryReference, + getWrappedPromise, + unwrapQueryRef, + updateWrappedQueryRef, + wrapQueryRef, +} from "./cache/QueryReference.js"; +export type { SuspenseCacheOptions } from "./cache/SuspenseCache.js"; diff --git a/src/react/parser/index.ts b/src/react/parser/index.ts index af678dc60d0..6bcf2989989 100644 --- a/src/react/parser/index.ts +++ b/src/react/parser/index.ts @@ -6,6 +6,12 @@ import type { VariableDefinitionNode, OperationDefinitionNode, } from "graphql"; +import { + AutoCleanedWeakCache, + cacheSizes, + defaultCacheSizes, +} from "../../utilities/index.js"; +import { registerGlobalCache } from "../../utilities/caching/getMemoryInternals.js"; export enum DocumentType { Query, @@ -19,7 +25,16 @@ export interface IDocumentDefinition { variables: ReadonlyArray; } -const cache = new Map(); +let cache: + | undefined + | AutoCleanedWeakCache< + DocumentNode, + { + name: string; + type: DocumentType; + variables: readonly VariableDefinitionNode[]; + } + >; export function operationName(type: DocumentType) { let name; @@ -39,6 +54,11 @@ export function operationName(type: DocumentType) { // This parser is mostly used to safety check incoming documents. export function parser(document: DocumentNode): IDocumentDefinition { + if (!cache) { + cache = new AutoCleanedWeakCache( + cacheSizes.parser || defaultCacheSizes.parser + ); + } const cached = cache.get(document); if (cached) return cached; @@ -130,6 +150,14 @@ export function parser(document: DocumentNode): IDocumentDefinition { return payload; } +parser.resetCache = () => { + cache = undefined; +}; + +if (__DEV__) { + registerGlobalCache("parser", () => (cache ? cache.size : 0)); +} + export function verifyDocumentType(document: DocumentNode, type: DocumentType) { const operation = parser(document); const requiredOperationName = operationName(type); 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..9b88fd385c9 --- /dev/null +++ b/src/react/query-preloader/__tests__/createQueryPreloader.test.tsx @@ -0,0 +1,2508 @@ +import React, { Suspense } from "react"; +import { createQueryPreloader } from "../createQueryPreloader"; +import { + ApolloClient, + ApolloError, + ApolloLink, + InMemoryCache, + NetworkStatus, + OperationVariables, + TypedDocumentNode, + gql, +} from "../../../core"; +import { + MockLink, + MockSubscriptionLink, + MockedResponse, + wait, +} from "../../../testing"; +import { expectTypeOf } from "expect-type"; +import { QueryReference, unwrapQueryRef } from "../../internal"; +import { DeepPartial, Observable } from "../../../utilities"; +import { + SimpleCaseData, + createProfiler, + spyOnConsole, + setupSimpleCase, + useTrackRenders, + setupVariablesCase, + renderWithClient, + VariablesCaseData, +} from "../../../testing/internal"; +import { ApolloProvider } from "../../context"; +import { act, render, renderHook, screen } from "@testing-library/react"; +import { UseReadQueryResult, useReadQuery } from "../../hooks"; +import { GraphQLError } from "graphql"; +import { ErrorBoundary } from "react-error-boundary"; +import userEvent from "@testing-library/user-event"; + +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 } = setupSimpleCase(); + 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 } = setupVariablesCase(); + 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: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } +}); + +test("Auto disposes of the query ref if not retained within the given time", async () => { + jest.useFakeTimers(); + const { query, mocks } = setupSimpleCase(); + 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 } = setupSimpleCase(); + 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 } = setupSimpleCase(); + + 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("useReadQuery auto-resubscribes the query after its disposed", async () => { + const { query } = setupSimpleCase(); + + let fetchCount = 0; + const link = new ApolloLink((operation) => { + let count = ++fetchCount; + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { greeting: `Hello ${count}` } }); + observer.complete(); + }, 100); + }); + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + + return ( + <> + + }> + {show && } + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await wait(0); + await Profiler.takeRender(); + + expect(queryRef).toBeDisposed(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // Ensure we aren't refetching the data by checking we still render the same + // cache result + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + client.writeQuery({ query, data: { greeting: "Hello (cached)" } }); + + // Ensure we can get cache updates again after remounting + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello (cached)" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Write a cache result to ensure that remounting will read this result + // instead of the old one + client.writeQuery({ query, data: { greeting: "While you were away" } }); + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(queryRef).not.toBeDisposed(); + + // Ensure we read the newest cache result changed while this queryRef was + // disposed + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "While you were away" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Remove cached data to ensure remounting will refetch the data + client.cache.modify({ + fields: { + greeting: (_, { DELETE }) => DELETE, + }, + }); + + // we wait a moment to ensure no network request is triggered + // by the `cache.modify` (even with a slight delay) + await wait(10); + expect(fetchCount).toBe(1); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // this should now trigger a network request + expect(fetchCount).toBe(2); + expect(queryRef).not.toBeDisposed(); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 2" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("useReadQuery handles auto-resubscribe with returnPartialData", async () => { + const { query, mocks } = setupVariablesCase(); + + let fetchCount = 0; + const link = new ApolloLink((operation) => { + fetchCount++; + const mock = mocks.find( + (mock) => mock.request.variables?.id === operation.variables.id + ); + + if (!mock) { + throw new Error("Could not find mock for variables"); + } + + const result = mock.result as Record; + + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: result.data }); + observer.complete(); + }, 100); + }); + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult> | null, + }, + }); + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { + returnPartialData: true, + variables: { id: "1" }, + }); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + + return ( + <> + + }> + {show && } + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await wait(0); + await Profiler.takeRender(); + + expect(queryRef).toBeDisposed(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // Ensure we aren't refetching the data by checking we still render the same + // cache result + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + client.writeQuery({ + query, + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (cached)", + }, + }, + variables: { id: "1" }, + }); + + // Ensure we can get cache updates again after remounting + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (cached)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Write a cache result to ensure that remounting will read this result + // instead of the old one + client.writeQuery({ + query, + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (Away)", + }, + }, + variables: { id: "1" }, + }); + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(queryRef).not.toBeDisposed(); + + // Ensure we read the newest cache result changed while this queryRef was + // disposed + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { + __typename: "Character", + id: "1", + name: "Spider-Man (Away)", + }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Remove cached data to ensure remounting will refetch the data + client.cache.modify({ + id: "Character:1", + fields: { + name: (_, { DELETE }) => DELETE, + }, + }); + + // we wait a moment to ensure no network request is triggered + // by the `cache.modify` (even with a slight delay) + await wait(10); + expect(fetchCount).toBe(1); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // this should now trigger a network request + expect(fetchCount).toBe(2); + expect(queryRef).not.toBeDisposed(); + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { character: { __typename: "Character", id: "1" } }, + error: undefined, + networkStatus: NetworkStatus.loading, + }); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + // Ensure that remounting without data in the cache will fetch and suspend + client.clearStore(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(fetchCount).toBe(3); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "Character", id: "1", name: "Spider-Man" }, + }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("useReadQuery handles auto-resubscribe on network-only fetch policy", async () => { + const { query } = setupSimpleCase(); + + let fetchCount = 0; + const link = new ApolloLink((operation) => { + let count = ++fetchCount; + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { greeting: `Hello ${count}` } }); + observer.complete(); + }, 10); + }); + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { fetchPolicy: "network-only" }); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + + return ( + <> + + }> + {show && } + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await wait(0); + await Profiler.takeRender(); + + expect(queryRef).toBeDisposed(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // Ensure we aren't refetching the data by checking we still render the same + // cache result + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + client.writeQuery({ query, data: { greeting: "Hello (cached)" } }); + + // Ensure we can get cache updates again after remounting + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello (cached)" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Write a cache result to ensure that remounting will read this result + // instead of the old one + client.writeQuery({ query, data: { greeting: "While you were away" } }); + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(queryRef).not.toBeDisposed(); + + // Ensure we read the newest cache result changed while this queryRef was + // disposed + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "While you were away" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Remove cached data to ensure remounting will refetch the data + client.cache.modify({ + fields: { + greeting: (_, { DELETE }) => DELETE, + }, + }); + + // Ensure the delete doesn't immediately fetch + await wait(10); + expect(fetchCount).toBe(1); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(fetchCount).toBe(2); + expect(queryRef).not.toBeDisposed(); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 2" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("useReadQuery handles auto-resubscribe on cache-and-network fetch policy", async () => { + const { query } = setupSimpleCase(); + + let fetchCount = 0; + const link = new ApolloLink((operation) => { + let count = ++fetchCount; + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { greeting: `Hello ${count}` } }); + observer.complete(); + }, 10); + }); + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { fetchPolicy: "cache-and-network" }); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + + return ( + <> + + }> + {show && } + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await wait(0); + await Profiler.takeRender(); + + expect(queryRef).toBeDisposed(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // Ensure we aren't refetching the data by checking we still render the same + // cache result + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + client.writeQuery({ query, data: { greeting: "Hello (cached)" } }); + + // Ensure we can get cache updates again after remounting + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello (cached)" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Write a cache result to ensure that remounting will read this result + // instead of the old one + client.writeQuery({ query, data: { greeting: "While you were away" } }); + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(queryRef).not.toBeDisposed(); + + // Ensure we read the newest cache result changed while this queryRef was + // disposed + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "While you were away" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Remove cached data to ensure remounting will refetch the data + client.cache.modify({ + fields: { + greeting: (_, { DELETE }) => DELETE, + }, + }); + + // Ensure delete doesn't refetch immediately + await wait(10); + expect(fetchCount).toBe(1); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(fetchCount).toBe(2); + expect(queryRef).not.toBeDisposed(); + + { + const { renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, SuspenseFallback]); + } + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 2" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("useReadQuery handles auto-resubscribe on no-cache fetch policy", async () => { + const { query } = setupSimpleCase(); + + let fetchCount = 0; + const link = new ApolloLink((operation) => { + let count = ++fetchCount; + return new Observable((observer) => { + setTimeout(() => { + observer.next({ data: { greeting: `Hello ${count}` } }); + observer.complete(); + }, 10); + }); + }); + + const Profiler = createProfiler({ + initialSnapshot: { + result: null as UseReadQueryResult | null, + }, + }); + const user = userEvent.setup(); + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + const preloadQuery = createQueryPreloader(client); + + const queryRef = preloadQuery(query, { fetchPolicy: "no-cache" }); + + function SuspenseFallback() { + useTrackRenders(); + return
Loading
; + } + + function App() { + useTrackRenders(); + const [show, setShow] = React.useState(true); + + return ( + <> + + }> + {show && } + + + ); + } + + function ReadQueryHook() { + useTrackRenders(); + Profiler.mergeSnapshot({ result: useReadQuery(queryRef) }); + + return null; + } + + renderWithClient(, { client, wrapper: Profiler }); + + const toggleButton = screen.getByText("Toggle"); + + // initial render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await wait(0); + await Profiler.takeRender(); + + expect(queryRef).toBeDisposed(); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + // Ensure we aren't refetching the data by checking we still render the same + // result + { + const { renderedComponents, snapshot } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + // Ensure caches writes for the query are ignored by the hook + client.writeQuery({ query, data: { greeting: "Hello (cached)" } }); + + await expect(Profiler).not.toRerender(); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Write a cache result to ensure that remounting will ignore this result + client.writeQuery({ query, data: { greeting: "While you were away" } }); + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(queryRef).not.toBeDisposed(); + + // Ensure we continue to read the same value + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + expect(fetchCount).toBe(1); + + // unmount ReadQueryHook + await act(() => user.click(toggleButton)); + await Profiler.takeRender(); + await wait(0); + + expect(queryRef).toBeDisposed(); + + // Remove cached data to verify this type of cache change is also ignored + client.cache.modify({ + fields: { + greeting: (_, { DELETE }) => DELETE, + }, + }); + + // Ensure delete doesn't fire off request + await wait(10); + expect(fetchCount).toBe(1); + + // mount ReadQueryHook + await act(() => user.click(toggleButton)); + + expect(fetchCount).toBe(1); + expect(queryRef).not.toBeDisposed(); + + // Ensure we are still rendering the same result and haven't refetched + // anything based on missing cache data + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual([App, ReadQueryHook]); + expect(snapshot.result).toEqual({ + data: { greeting: "Hello 1" }, + error: undefined, + networkStatus: NetworkStatus.ready, + }); + } + + await expect(Profiler).not.toRerender(); +}); + +test("reacts to cache updates", async () => { + const { query, mocks } = setupSimpleCase(); + 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 } = setupSimpleCase(); + + 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 } = setupSimpleCase(); + + 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 } = setupSimpleCase(); + + 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 } = setupSimpleCase(); + + 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 } = setupSimpleCase(); + 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 } = setupSimpleCase(); + 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 } = setupSimpleCase(); + 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 calling preloadQuery with the same query", async () => { + const { query } = setupSimpleCase(); + + 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 unwrappedQueryRef1 = unwrapQueryRef(queryRef1); + const unwrappedQueryRef2 = unwrapQueryRef(queryRef2); + + // Use Object.is inside expect to prevent circular reference errors on toBe + expect(Object.is(queryRef1, queryRef2)).toBe(false); + expect(Object.is(unwrappedQueryRef1, unwrappedQueryRef2)).toBe(false); + + await expect(queryRef1.toPromise()).resolves.toBe(queryRef1); + await expect(queryRef2.toPromise()).resolves.toBe(queryRef2); +}); + +test("does not suspend and returns partial data when `returnPartialData` is `true`", async () => { + const { query, mocks } = setupVariablesCase(); + const partialQuery = gql` + query CharacterQuery($id: ID!) { + character(id: $id) { + id + } + } + `; + + const client = createDefaultClient(mocks); + + client.writeQuery({ + query: partialQuery, + data: { character: { __typename: "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: { __typename: "Character", id: "1" } }, + networkStatus: NetworkStatus.loading, + error: undefined, + }); + } + + { + const { snapshot, renderedComponents } = await Profiler.takeRender(); + + expect(renderedComponents).toStrictEqual(["ReadQueryHook"]); + expect(snapshot.result).toEqual({ + data: { + character: { __typename: "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); + // @ts-expect-error missing variables option + preloadQuery(query, { returnPartialData: true }); + // @ts-expect-error missing variables option + preloadQuery(query, { returnPartialData: true }); + 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, {}); + // @ts-expect-error missing variables option + preloadQuery(query, { returnPartialData: true }); + // @ts-expect-error missing variables option + preloadQuery(query, { returnPartialData: true }); + 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< + QueryReference + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + }); + + 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< + QueryReference + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + }); + + test("returns QueryReference> with returnPartialData: true", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { returnPartialData: true }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, { [key: string]: any }> + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: true, + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, OperationVariables> + >(); + } + }); + + 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< + QueryReference + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + canonizeResults: true, + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference + >(); + } + }); + + test("handles combinations of options", () => { + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference< + DeepPartial | undefined, + { [key: string]: any } + > + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: true, + errorPolicy: "ignore", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference< + DeepPartial | undefined, + OperationVariables + > + >(); + } + + { + const query: TypedDocumentNode = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, { [key: string]: any }> + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, OperationVariables> + >(); + } + }); + + 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, { [key: string]: any }> + >(); + } + + { + const query = gql``; + const queryRef = preloadQuery(query, { + canonizeResults: true, + returnPartialData: true, + errorPolicy: "none", + }); + + expectTypeOf(queryRef).toEqualTypeOf< + QueryReference, OperationVariables> + >(); + } + }); +}); diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts new file mode 100644 index 00000000000..b7a9d22afcb --- /dev/null +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -0,0 +1,193 @@ +import type { + ApolloClient, + DefaultContext, + DocumentNode, + ErrorPolicy, + OperationVariables, + RefetchWritePolicy, + TypedDocumentNode, + WatchQueryFetchPolicy, + WatchQueryOptions, +} from "../../core/index.js"; +import type { + DeepPartial, + OnlyRequiredProperties, +} from "../../utilities/index.js"; +import { InternalQueryReference, wrapQueryRef } from "../internal/index.js"; +import type { QueryReference } from "../internal/index.js"; +import type { NoInfer } from "../index.js"; + +type VariablesOption = + [TVariables] extends [never] ? + { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables?: Record; + } + : {} extends OnlyRequiredProperties ? + { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables?: TVariables; + } + : { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables: TVariables; + }; + +export type PreloadQueryFetchPolicy = Extract< + WatchQueryFetchPolicy, + "cache-first" | "network-only" | "no-cache" | "cache-and-network" +>; + +export type PreloadQueryOptions< + TVariables extends OperationVariables = OperationVariables, +> = { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ + canonizeResults?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ + context?: DefaultContext; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ + errorPolicy?: ErrorPolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ + fetchPolicy?: PreloadQueryFetchPolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#returnPartialData:member} */ + returnPartialData?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#refetchWritePolicy:member} */ + refetchWritePolicy?: RefetchWritePolicy; +} & VariablesOption; + +type PreloadQueryOptionsArg< + TVariables extends OperationVariables, + TOptions = unknown, +> = [TVariables] extends [never] ? + [options?: PreloadQueryOptions & TOptions] +: {} extends OnlyRequiredProperties ? + [ + options?: PreloadQueryOptions> & + Omit, + ] +: [ + options: PreloadQueryOptions> & + Omit, + ]; + +/** + * A function that will begin loading a query when called. It's result can be + * read by {@link useReadQuery} which will suspend until the query is loaded. + * This is useful when you want to start loading a query as early as possible + * outside of a React component. + * + * @example + * ```js + * const preloadQuery = createQueryPreloader(client); + * const queryRef = preloadQuery(query, { variables, ...otherOptions }); + * + * function App() { + * return ( + * Loading}> + * + * + * ); + * } + * + * function MyQuery() { + * const { data } = useReadQuery(queryRef); + * + * // do something with `data` + * } + * ``` + */ +export interface PreloadQueryFunction { + /** {@inheritDoc @apollo/client!PreloadQueryFunction:interface} */ + < + 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 + >; + + /** {@inheritDoc @apollo/client!PreloadQueryFunction:interface} */ + ( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & { + returnPartialData: true; + errorPolicy: "ignore" | "all"; + } + ): QueryReference | undefined, TVariables>; + + /** {@inheritDoc @apollo/client!PreloadQueryFunction:interface} */ + ( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & { + errorPolicy: "ignore" | "all"; + } + ): QueryReference; + + /** {@inheritDoc @apollo/client!PreloadQueryFunction:interface} */ + ( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & { + returnPartialData: true; + } + ): QueryReference, TVariables>; + + /** {@inheritDoc @apollo/client!PreloadQueryFunction:interface} */ + ( + query: DocumentNode | TypedDocumentNode, + ...[options]: PreloadQueryOptionsArg> + ): QueryReference; +} + +/** + * A higher order function that returns a `preloadQuery` function which + * can be used to begin loading a query with the given `client`. This is useful + * when you want to start loading a query as early as possible outside of a + * React component. + * + * > Refer to the [Suspense - Initiating queries outside React](https://www.apollographql.com/docs/react/data/suspense#initiating-queries-outside-react) section for a more in-depth overview. + * + * @param client - The `ApolloClient` instance that will be used to load queries + * from the returned `preloadQuery` function. + * @returns The `preloadQuery` function. + * + * @example + * ```js + * const preloadQuery = createQueryPreloader(client); + * ``` + * @since 3.9.0 + * @alpha + */ +export function createQueryPreloader( + client: ApolloClient +): PreloadQueryFunction { + return function preloadQuery< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, + >( + query: DocumentNode | TypedDocumentNode, + options: PreloadQueryOptions> & + VariablesOption = Object.create(null) + ): QueryReference { + const queryRef = new InternalQueryReference( + client.watchQuery({ + ...options, + query, + } as WatchQueryOptions), + { + autoDisposeTimeoutMs: + client.defaultOptions.react?.suspense?.autoDisposeTimeoutMs, + } + ); + + return wrapQueryRef(queryRef); + }; +} diff --git a/src/react/ssr/RenderPromises.ts b/src/react/ssr/RenderPromises.ts index d72574bd6c1..f51bcc93aca 100644 --- a/src/react/ssr/RenderPromises.ts +++ b/src/react/ssr/RenderPromises.ts @@ -1,4 +1,5 @@ import type { DocumentNode } from "graphql"; +import type * as ReactTypes from "react"; import type { ObservableQuery, OperationVariables } from "../../core/index.js"; import type { QueryDataOptions } from "../types/types.js"; @@ -58,8 +59,8 @@ export class RenderPromises { public addQueryPromise( queryInstance: QueryData, - finish?: () => React.ReactNode - ): React.ReactNode { + finish?: () => ReactTypes.ReactNode + ): ReactTypes.ReactNode { if (!this.stopped) { const info = this.lookupQueryInfo(queryInstance.getOptions()); if (!info.seen) { diff --git a/src/react/ssr/getDataFromTree.ts b/src/react/ssr/getDataFromTree.ts index 5d8b34829d0..4dde58ec62a 100644 --- a/src/react/ssr/getDataFromTree.ts +++ b/src/react/ssr/getDataFromTree.ts @@ -1,10 +1,11 @@ -import * as React from "react"; +import * as React from "rehackt"; +import type * as ReactTypes from "react"; import { getApolloContext } from "../context/index.js"; import { RenderPromises } from "./RenderPromises.js"; import { renderToStaticMarkup } from "react-dom/server"; export function getDataFromTree( - tree: React.ReactNode, + tree: ReactTypes.ReactNode, context: { [key: string]: any } = {} ) { return getMarkupFromTree({ @@ -17,10 +18,10 @@ export function getDataFromTree( } export type GetMarkupFromTreeOptions = { - tree: React.ReactNode; + tree: ReactTypes.ReactNode; context?: { [key: string]: any }; renderFunction?: ( - tree: React.ReactElement + tree: ReactTypes.ReactElement ) => string | PromiseLike; }; diff --git a/src/react/ssr/renderToStringWithData.ts b/src/react/ssr/renderToStringWithData.ts index 0e3944344a2..f6bcb345849 100644 --- a/src/react/ssr/renderToStringWithData.ts +++ b/src/react/ssr/renderToStringWithData.ts @@ -1,9 +1,9 @@ -import type { ReactElement } from "react"; +import type * as ReactTypes from "react"; import { getMarkupFromTree } from "./getDataFromTree.js"; import { renderToString } from "react-dom/server"; export function renderToStringWithData( - component: ReactElement + component: ReactTypes.ReactElement ): Promise { return getMarkupFromTree({ tree: component, diff --git a/src/react/types/types.documentation.ts b/src/react/types/types.documentation.ts new file mode 100644 index 00000000000..364e8e3f188 --- /dev/null +++ b/src/react/types/types.documentation.ts @@ -0,0 +1,598 @@ +export interface QueryOptionsDocumentation { + /** + * A GraphQL query string parsed into an AST with the gql template literal. + * + * @docGroup 1. Operation options + */ + query: unknown; + + /** + * An object containing all of the GraphQL variables your query requires to execute. + * + * Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. + * + * @docGroup 1. Operation options + */ + variables: unknown; + + /** + * Specifies how the query handles a response that returns both GraphQL errors and partial results. + * + * For details, see [GraphQL error policies](https://www.apollographql.com/docs/react/data/error-handling/#graphql-error-policies). + * + * The default value is `none`, meaning that the query result includes error details but not partial results. + * + * @docGroup 1. Operation options + */ + errorPolicy: unknown; + + /** + * If you're using [Apollo Link](https://www.apollographql.com/docs/react/api/link/introduction/), this object is the initial value of the `context` object that's passed along your link chain. + * + * @docGroup 2. Networking options + */ + context: unknown; + + /** + * Specifies how the query interacts with the Apollo Client cache during execution (for example, whether it checks the cache for results before sending a request to the server). + * + * For details, see [Setting a fetch policy](https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy). + * + * The default value is `cache-first`. + * + * @docGroup 3. Caching options + */ + fetchPolicy: unknown; + + /** + * Specifies the {@link FetchPolicy} to be used after this query has completed. + * + * @docGroup 3. Caching options + */ + nextFetchPolicy: unknown; + + /** + * Defaults to the initial value of options.fetchPolicy, but can be explicitly + * configured to specify the WatchQueryFetchPolicy to revert back to whenever + * variables change (unless nextFetchPolicy intervenes). + * + * @docGroup 3. Caching options + */ + initialFetchPolicy: unknown; + + /** + * Specifies the interval (in milliseconds) at which the query polls for updated results. + * + * The default value is `0` (no polling). + * + * @docGroup 2. Networking options + */ + pollInterval: unknown; + + /** + * If `true`, the in-progress query's associated component re-renders whenever the network status changes or a network error occurs. + * + * The default value is `false`. + * + * @docGroup 2. Networking options + */ + notifyOnNetworkStatusChange: unknown; + + /** + * If `true`, the query can return partial results from the cache if the cache doesn't contain results for all queried fields. + * + * The default value is `false`. + * + * @docGroup 3. Caching options + */ + returnPartialData: unknown; + + /** + * Specifies whether a `NetworkStatus.refetch` operation should merge + * incoming field data with existing data, or overwrite the existing data. + * Overwriting is probably preferable, but merging is currently the default + * behavior, for backwards compatibility with Apollo Client 3.x. + * + * @docGroup 3. Caching options + */ + refetchWritePolicy: unknown; + + /** + * Watched queries must opt into overwriting existing data on refetch, by passing refetchWritePolicy: "overwrite" in their WatchQueryOptions. + * + * The default value is "overwrite". + * + * @docGroup 3. Caching options + */ + refetchWritePolicy_suspense: unknown; + + /** + * If `true`, causes a query refetch if the query result is detected as partial. + * + * The default value is `false`. + * + * @deprecated + * Setting this option is unnecessary in Apollo Client 3, thanks to a more consistent application of fetch policies. It might be removed in a future release. + */ + partialRefetch: unknown; + + /** + * Whether to canonize cache results before returning them. Canonization + * takes some extra time, but it speeds up future deep equality comparisons. + * Defaults to false. + * + * @deprecated + * Using `canonizeResults` can result in memory leaks so we generally do not + * recommend using this option anymore. + * A future version of Apollo Client will contain a similar feature without + * the risk of memory leaks. + */ + canonizeResults: unknown; + + /** + * If true, the query is not executed. + * + * The default value is `false`. + * + * @docGroup 1. Operation options + */ + skip: unknown; + + /** + * If `true`, the query is not executed. The default value is `false`. + * + * @deprecated We recommend using `skipToken` in place of the `skip` option as + * it is more type-safe. + * + * This option is deprecated and only supported to ease the migration from useQuery. It will be removed in a future release. + * + * @docGroup 1. Operation options + */ + skip_deprecated: unknown; + + /** + * A callback function that's called when your query successfully completes with zero errors (or if `errorPolicy` is `ignore` and partial data is returned). + * + * This function is passed the query's result `data`. + * + * @docGroup 1. Operation options + */ + onCompleted: unknown; + /** + * A callback function that's called when the query encounters one or more errors (unless `errorPolicy` is `ignore`). + * + * This function is passed an `ApolloError` object that contains either a `networkError` object or a `graphQLErrors` array, depending on the error(s) that occurred. + * + * @docGroup 1. Operation options + */ + onError: unknown; + + /** + * The instance of {@link ApolloClient} to use to execute the query. + * + * By default, the instance that's passed down via context is used, but you + * can provide a different instance here. + * + * @docGroup 1. Operation options + */ + client: unknown; + + /** + * A unique identifier for the query. Each item in the array must be a stable + * identifier to prevent infinite fetches. + * + * This is useful when using the same query and variables combination in more + * than one component, otherwise the components may clobber each other. This + * can also be used to force the query to re-evaluate fresh. + * + * @docGroup 1. Operation options + */ + queryKey: unknown; + + /** + * Pass `false` to skip executing the query during [server-side rendering](https://www.apollographql.com/docs/react/performance/server-side-rendering/). + * + * @docGroup 2. Networking options + */ + ssr: unknown; + + /** + * A callback function that's called whenever a refetch attempt occurs + * while polling. If the function returns `true`, the refetch is + * skipped and not reattempted until the next poll interval. + * + * @docGroup 2. Networking options + */ + skipPollAttempt: unknown; +} + +export interface QueryResultDocumentation { + /** + * The instance of Apollo Client that executed the query. + * Can be useful for manually executing followup queries or writing data to the cache. + * + * @docGroup 2. Network info + */ + client: unknown; + /** + * A reference to the internal `ObservableQuery` used by the hook. + */ + observable: unknown; + /** + * An object containing the result of your GraphQL query after it completes. + * + * This value might be `undefined` if a query results in one or more errors (depending on the query's `errorPolicy`). + * + * @docGroup 1. Operation data + */ + data: unknown; + /** + * An object containing the result from the most recent _previous_ execution of this query. + * + * This value is `undefined` if this is the query's first execution. + * + * @docGroup 1. Operation data + */ + previousData: unknown; + /** + * If the query produces one or more errors, this object contains either an array of `graphQLErrors` or a single `networkError`. Otherwise, this value is `undefined`. + * + * For more information, see [Handling operation errors](https://www.apollographql.com/docs/react/data/error-handling/). + * + * @docGroup 1. Operation data + */ + error: unknown; + /** + * If `true`, the query is still in flight and results have not yet been returned. + * + * @docGroup 2. Network info + */ + loading: unknown; + /** + * A number indicating the current network state of the query's associated request. [See possible values.](https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/core/networkStatus.ts#L4) + * + * Used in conjunction with the [`notifyOnNetworkStatusChange`](#notifyonnetworkstatuschange) option. + * + * @docGroup 2. Network info + */ + networkStatus: unknown; + /** + * If `true`, the associated lazy query has been executed. + * + * This field is only present on the result object returned by [`useLazyQuery`](/react/data/queries/#executing-queries-manually). + * + * @docGroup 2. Network info + */ + called: unknown; + /** + * An object containing the variables that were provided for the query. + * + * @docGroup 1. Operation data + */ + variables: unknown; + + /** + * A function that enables you to re-execute the query, optionally passing in new `variables`. + * + * To guarantee that the refetch performs a network request, its `fetchPolicy` is set to `network-only` (unless the original query's `fetchPolicy` is `no-cache` or `cache-and-network`, which also guarantee a network request). + * + * See also [Refetching](https://www.apollographql.com/docs/react/data/queries/#refetching). + * + * @docGroup 3. Helper functions + */ + refetch: unknown; + /** + * {@inheritDoc @apollo/client!ObservableQuery#fetchMore:member(1)} + * + * @docGroup 3. Helper functions + */ + fetchMore: unknown; + /** + * {@inheritDoc @apollo/client!ObservableQuery#startPolling:member(1)} + * + * @docGroup 3. Helper functions + */ + startPolling: unknown; + /** + * {@inheritDoc @apollo/client!ObservableQuery#stopPolling:member(1)} + * + * @docGroup 3. Helper functions + */ + stopPolling: unknown; + /** + * {@inheritDoc @apollo/client!ObservableQuery#subscribeToMore:member(1)} + * + * @docGroup 3. Helper functions + */ + subscribeToMore: unknown; + /** + * {@inheritDoc @apollo/client!ObservableQuery#updateQuery:member(1)} + * + * @docGroup 3. Helper functions + */ + updateQuery: unknown; +} + +export interface MutationOptionsDocumentation { + /** + * A GraphQL document, often created with `gql` from the `graphql-tag` + * package, that contains a single mutation inside of it. + * + * @docGroup 1. Operation options + */ + mutation: unknown; + + /** + * Provide `no-cache` if the mutation's result should _not_ be written to the Apollo Client cache. + * + * The default value is `network-only` (which means the result _is_ written to the cache). + * + * Unlike queries, mutations _do not_ support [fetch policies](https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy) besides `network-only` and `no-cache`. + * + * @docGroup 3. Caching options + */ + fetchPolicy: unknown; + + /** + * To avoid retaining sensitive information from mutation root field + * arguments, Apollo Client v3.4+ automatically clears any `ROOT_MUTATION` + * fields from the cache after each mutation finishes. If you need this + * information to remain in the cache, you can prevent the removal by passing + * `keepRootFields: true` to the mutation. `ROOT_MUTATION` result data are + * also passed to the mutation `update` function, so we recommend obtaining + * the results that way, rather than using this option, if possible. + */ + keepRootFields: unknown; + + /** + * By providing either an object or a callback function that, when invoked after + * a mutation, allows you to return optimistic data and optionally skip updates + * via the `IGNORE` sentinel object, Apollo Client caches this temporary + * (and potentially incorrect) response until the mutation completes, enabling + * more responsive UI updates. + * + * For more information, see [Optimistic mutation results](https://www.apollographql.com/docs/react/performance/optimistic-ui/). + * + * @docGroup 3. Caching options + */ + optimisticResponse: unknown; + + /** + * A {@link MutationQueryReducersMap}, which is map from query names to + * mutation query reducers. Briefly, this map defines how to incorporate the + * results of the mutation into the results of queries that are currently + * being watched by your application. + */ + updateQueries: unknown; + + /** + * An array (or a function that _returns_ an array) that specifies which queries you want to refetch after the mutation occurs. + * + * Each array value can be either: + * + * - An object containing the `query` to execute, along with any `variables` + * + * - A string indicating the operation name of the query to refetch + * + * @docGroup 1. Operation options + */ + refetchQueries: unknown; + + /** + * If `true`, makes sure all queries included in `refetchQueries` are completed before the mutation is considered complete. + * + * The default value is `false` (queries are refetched asynchronously). + * + * @docGroup 1. Operation options + */ + awaitRefetchQueries: unknown; + + /** + * A function used to update the Apollo Client cache after the mutation completes. + * + * For more information, see [Updating the cache after a mutation](https://www.apollographql.com/docs/react/data/mutations#updating-the-cache-after-a-mutation). + * + * @docGroup 3. Caching options + */ + update: unknown; + + /** + * Optional callback for intercepting queries whose cache data has been updated by the mutation, as well as any queries specified in the `refetchQueries: [...]` list passed to `client.mutate`. + * + * Returning a `Promise` from `onQueryUpdated` will cause the final mutation `Promise` to await the returned `Promise`. Returning `false` causes the query to be ignored. + * + * @docGroup 1. Operation options + */ + onQueryUpdated: unknown; + + /** + * Specifies how the mutation handles a response that returns both GraphQL errors and partial results. + * + * For details, see [GraphQL error policies](https://www.apollographql.com/docs/react/data/error-handling/#graphql-error-policies). + * + * The default value is `none`, meaning that the mutation result includes error details but _not_ partial results. + * + * @docGroup 1. Operation options + */ + errorPolicy: unknown; + + /** + * An object containing all of the GraphQL variables your mutation requires to execute. + * + * Each key in the object corresponds to a variable name, and that key's value corresponds to the variable value. + * + * @docGroup 1. Operation options + */ + variables: unknown; + + /** + * If you're using [Apollo Link](https://www.apollographql.com/docs/react/api/link/introduction/), this object is the initial value of the `context` object that's passed along your link chain. + * + * @docGroup 2. Networking options + */ + context: unknown; + + /** + * The instance of `ApolloClient` to use to execute the mutation. + * + * By default, the instance that's passed down via context is used, but you can provide a different instance here. + * + * @docGroup 2. Networking options + */ + client: unknown; + /** + * If `true`, the in-progress mutation's associated component re-renders whenever the network status changes or a network error occurs. + * + * The default value is `false`. + * + * @docGroup 2. Networking options + */ + notifyOnNetworkStatusChange: unknown; + /** + * A callback function that's called when your mutation successfully completes with zero errors (or if `errorPolicy` is `ignore` and partial data is returned). + * + * This function is passed the mutation's result `data` and any options passed to the mutation. + * + * @docGroup 1. Operation options + */ + onCompleted: unknown; + /** + * A callback function that's called when the mutation encounters one or more errors (unless `errorPolicy` is `ignore`). + * + * This function is passed an [`ApolloError`](https://github.com/apollographql/apollo-client/blob/d96f4578f89b933c281bb775a39503f6cdb59ee8/src/errors/index.ts#L36-L39) object that contains either a `networkError` object or a `graphQLErrors` array, depending on the error(s) that occurred, as well as any options passed the mutation. + * + * @docGroup 1. Operation options + */ + onError: unknown; + /** + * If `true`, the mutation's `data` property is not updated with the mutation's result. + * + * The default value is `false`. + * + * @docGroup 1. Operation options + */ + ignoreResults: unknown; +} + +export interface MutationResultDocumentation { + /** + * The data returned from your mutation. Can be `undefined` if `ignoreResults` is `true`. + */ + data: unknown; + /** + * If the mutation produces one or more errors, this object contains either an array of `graphQLErrors` or a single `networkError`. Otherwise, this value is `undefined`. + * + * For more information, see [Handling operation errors](https://www.apollographql.com/docs/react/data/error-handling/). + */ + error: unknown; + /** + * If `true`, the mutation is currently in flight. + */ + loading: unknown; + /** + * If `true`, the mutation's mutate function has been called. + */ + called: unknown; + /** + * The instance of Apollo Client that executed the mutation. + * + * Can be useful for manually executing followup operations or writing data to the cache. + */ + client: unknown; + /** + * A function that you can call to reset the mutation's result to its initial, uncalled state. + */ + reset: unknown; +} + +export interface SubscriptionOptionsDocumentation { + /** + * A GraphQL document, often created with `gql` from the `graphql-tag` + * package, that contains a single subscription inside of it. + */ + query: unknown; + /** + * An object containing all of the variables your subscription needs to execute + */ + variables: unknown; + + /** + * Specifies the {@link ErrorPolicy} to be used for this operation + */ + errorPolicy: unknown; + + /** + * How you want your component to interact with the Apollo cache. For details, see [Setting a fetch policy](https://www.apollographql.com/docs/react/data/queries/#setting-a-fetch-policy). + */ + fetchPolicy: unknown; + + /** + * Determines if your subscription should be unsubscribed and subscribed again when an input to the hook (such as `subscription` or `variables`) changes. + */ + shouldResubscribe: unknown; + + /** + * An `ApolloClient` instance. By default `useSubscription` / `Subscription` uses the client passed down via context, but a different client can be passed in. + */ + client: unknown; + + /** + * Determines if the current subscription should be skipped. Useful if, for example, variables depend on previous queries and are not ready yet. + */ + skip: unknown; + + /** + * Shared context between your component and your network interface (Apollo Link). + */ + context: unknown; + + /** + * Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component completes the subscription. + * + * @since 3.7.0 + */ + onComplete: unknown; + + /** + * Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives data. The callback `options` object param consists of the current Apollo Client instance in `client`, and the received subscription data in `data`. + * + * @since 3.7.0 + */ + onData: unknown; + + /** + * Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives data. The callback `options` object param consists of the current Apollo Client instance in `client`, and the received subscription data in `subscriptionData`. + * + * @deprecated Use `onData` instead + */ + onSubscriptionData: unknown; + + /** + * Allows the registration of a callback function that will be triggered each time the `useSubscription` Hook / `Subscription` component receives an error. + * + * @since 3.7.0 + */ + onError: unknown; + + /** + * Allows the registration of a callback function that will be triggered when the `useSubscription` Hook / `Subscription` component completes the subscription. + * + * @deprecated Use `onComplete` instead + */ + onSubscriptionComplete: unknown; +} + +export interface SubscriptionResultDocumentation { + /** + * A boolean that indicates whether any initial data has been returned + */ + loading: unknown; + /** + * An object containing the result of your GraphQL subscription. Defaults to an empty object. + */ + data: unknown; + /** + * A runtime error with `graphQLErrors` and `networkError` properties + */ + error: unknown; +} diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 5d057261dbc..785c2ed793b 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -1,4 +1,4 @@ -import type { ReactNode } from "react"; +import type * as ReactTypes from "react"; import type { DocumentNode } from "graphql"; import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; @@ -13,18 +13,26 @@ import type { ApolloClient, DefaultContext, FetchPolicy, - MutationOptions, NetworkStatus, ObservableQuery, OperationVariables, InternalRefetchQueriesInclude, WatchQueryOptions, WatchQueryFetchPolicy, + SubscribeToMoreOptions, + ApolloQueryResult, + FetchMoreQueryOptions, + ErrorPolicy, + RefetchWritePolicy, } from "../../core/index.js"; +import type { + MutationSharedOptions, + SharedWatchQueryOptions, +} from "../../core/watchQueryOptions.js"; /* QueryReference type */ -export type { QueryReference } from "../cache/QueryReference.js"; +export type { QueryReference } from "../internal/index.js"; /* Common types */ @@ -38,18 +46,25 @@ export type CommonOptions = TOptions & { export interface BaseQueryOptions< TVariables extends OperationVariables = OperationVariables, -> extends Omit, "query"> { + TData = any, +> extends SharedWatchQueryOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#ssr:member} */ ssr?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#client:member} */ client?: ApolloClient; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; } export interface QueryFunctionOptions< TData = any, TVariables extends OperationVariables = OperationVariables, -> extends BaseQueryOptions { +> extends BaseQueryOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#skip:member} */ skip?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} */ onCompleted?: (data: TData) => void; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} */ onError?: (error: ApolloError) => void; // Default WatchQueryOptions for this useQuery, providing initial values for @@ -57,35 +72,81 @@ export interface QueryFunctionOptions< // by option, not whole), but never overriding options previously passed to // useQuery (or options added/modified later by other means). // TODO What about about default values that are expensive to evaluate? + /** @internal */ defaultOptions?: Partial>; } -export type ObservableQueryFields< +export interface ObservableQueryFields< TData, TVariables extends OperationVariables, -> = Pick< - ObservableQuery, - | "startPolling" - | "stopPolling" - | "subscribeToMore" - | "updateQuery" - | "refetch" - | "reobserve" - | "variables" - | "fetchMore" ->; +> { + /** {@inheritDoc @apollo/client!QueryResultDocumentation#startPolling:member} */ + startPolling(pollInterval: number): void; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#stopPolling:member} */ + stopPolling(): void; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#subscribeToMore:member} */ + subscribeToMore< + TSubscriptionData = TData, + TSubscriptionVariables extends OperationVariables = TVariables, + >( + options: SubscribeToMoreOptions< + TData, + TSubscriptionVariables, + TSubscriptionData + > + ): () => void; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#updateQuery:member} */ + updateQuery( + mapFn: ( + previousQueryResult: TData, + options: Pick, "variables"> + ) => TData + ): void; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#refetch:member} */ + refetch(variables?: Partial): Promise>; + /** @internal */ + reobserve( + newOptions?: Partial>, + newNetworkStatus?: NetworkStatus + ): Promise>; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#variables:member} */ + variables: TVariables | undefined; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#fetchMore:member} */ + fetchMore< + TFetchData = TData, + TFetchVars extends OperationVariables = TVariables, + >( + fetchMoreOptions: FetchMoreQueryOptions & { + updateQuery?: ( + previousQueryResult: TData, + options: { + fetchMoreResult: TFetchData; + variables: TFetchVars; + } + ) => TData; + } + ): Promise>; +} export interface QueryResult< TData = any, TVariables extends OperationVariables = OperationVariables, > extends ObservableQueryFields { + /** {@inheritDoc @apollo/client!QueryResultDocumentation#client:member} */ client: ApolloClient; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#observable:member} */ observable: ObservableQuery; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#data:member} */ data: TData | undefined; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#previousData:member} */ previousData?: TData; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#error:member} */ error?: ApolloError; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#loading:member} */ loading: boolean; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#networkStatus:member} */ networkStatus: NetworkStatus; + /** {@inheritDoc @apollo/client!QueryResultDocumentation#called:member} */ called: boolean; } @@ -93,7 +154,8 @@ export interface QueryDataOptions< TData = any, TVariables extends OperationVariables = OperationVariables, > extends QueryFunctionOptions { - children?: (result: QueryResult) => ReactNode; + children?: (result: QueryResult) => ReactTypes.ReactNode; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#query:member} */ query: DocumentNode | TypedDocumentNode; } @@ -105,8 +167,15 @@ export interface QueryHookOptions< export interface LazyQueryHookOptions< TData = any, TVariables extends OperationVariables = OperationVariables, -> extends Omit, "skip"> {} +> extends BaseQueryOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} */ + onCompleted?: (data: TData) => void; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} */ + onError?: (error: ApolloError) => void; + /** @internal */ + defaultOptions?: Partial>; +} export interface LazyQueryHookExecOptions< TData = any, TVariables extends OperationVariables = OperationVariables, @@ -122,24 +191,28 @@ export type SuspenseQueryHookFetchPolicy = Extract< export interface SuspenseQueryHookOptions< TData = unknown, TVariables extends OperationVariables = OperationVariables, -> extends Pick< - QueryHookOptions, - | "client" - | "variables" - | "errorPolicy" - | "context" - | "canonizeResults" - | "returnPartialData" - | "refetchWritePolicy" - > { +> { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#client:member} */ + client?: ApolloClient; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ + context?: DefaultContext; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables?: TVariables; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ + errorPolicy?: ErrorPolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ + canonizeResults?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#returnPartialData:member} */ + returnPartialData?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#refetchWritePolicy_suspense:member} */ + refetchWritePolicy?: RefetchWritePolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: SuspenseQueryHookFetchPolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#queryKey:member} */ queryKey?: string | number | any[]; /** - * If `true`, the query is not executed. The default value is `false`. - * - * @deprecated We recommend using `skipToken` in place of the `skip` option as - * it is more type-safe. + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#skip_deprecated:member} * * @example Recommended usage of `skipToken`: * ```ts @@ -173,10 +246,7 @@ export interface BackgroundQueryHookOptions< queryKey?: string | number | any[]; /** - * If `true`, the query is not executed. The default value is `false`. - * - * @deprecated We recommend using `skipToken` in place of the `skip` option as - * it is more type-safe. + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#skip_deprecated:member} * * @example Recommended usage of `skipToken`: * ```ts @@ -188,16 +258,42 @@ export interface BackgroundQueryHookOptions< skip?: boolean; } +export type LoadableQueryHookFetchPolicy = Extract< + WatchQueryFetchPolicy, + "cache-first" | "network-only" | "no-cache" | "cache-and-network" +>; + +export interface LoadableQueryHookOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#canonizeResults:member} */ + canonizeResults?: boolean; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#client:member} */ + client?: ApolloClient; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ + context?: DefaultContext; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#errorPolicy:member} */ + errorPolicy?: ErrorPolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#fetchPolicy:member} */ + fetchPolicy?: LoadableQueryHookFetchPolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#queryKey:member} */ + queryKey?: string | number | any[]; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#refetchWritePolicy:member} */ + refetchWritePolicy?: RefetchWritePolicy; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#returnPartialData:member} */ + returnPartialData?: boolean; +} + /** - * @deprecated TODO Delete this unused interface. + * @deprecated This type will be removed in the next major version of Apollo Client */ export interface QueryLazyOptions { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ variables?: TVariables; + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#context:member} */ context?: DefaultContext; } /** - * @deprecated TODO Delete this unused type alias. + * @deprecated This type will be removed in the next major version of Apollo Client */ export type LazyQueryResult< TData, @@ -205,7 +301,7 @@ export type LazyQueryResult< > = QueryResult; /** - * @deprecated TODO Delete this unused type alias. + * @deprecated This type will be removed in the next major version of Apollo Client */ export type QueryTuple< TData, @@ -222,7 +318,10 @@ export type LazyQueryExecFunction< export type LazyQueryResultTuple< TData, TVariables extends OperationVariables, -> = [LazyQueryExecFunction, QueryResult]; +> = [ + execute: LazyQueryExecFunction, + result: QueryResult, +]; /* Mutation types */ @@ -235,14 +334,16 @@ export interface BaseMutationOptions< TVariables = OperationVariables, TContext = DefaultContext, TCache extends ApolloCache = ApolloCache, -> extends Omit< - MutationOptions, - "mutation" - > { +> extends MutationSharedOptions { + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#client:member} */ client?: ApolloClient; + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#notifyOnNetworkStatusChange:member} */ notifyOnNetworkStatusChange?: boolean; + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#onCompleted:member} */ onCompleted?: (data: TData, clientOptions?: BaseMutationOptions) => void; + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#onError:member} */ onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#ignoreResults:member} */ ignoreResults?: boolean; } @@ -252,15 +353,22 @@ export interface MutationFunctionOptions< TContext = DefaultContext, TCache extends ApolloCache = ApolloCache, > extends BaseMutationOptions { + /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#mutation:member} */ mutation?: DocumentNode | TypedDocumentNode; } export interface MutationResult { + /** {@inheritDoc @apollo/client!MutationResultDocumentation#data:member} */ data?: TData | null; + /** {@inheritDoc @apollo/client!MutationResultDocumentation#error:member} */ error?: ApolloError; + /** {@inheritDoc @apollo/client!MutationResultDocumentation#loading:member} */ loading: boolean; + /** {@inheritDoc @apollo/client!MutationResultDocumentation#called:member} */ called: boolean; + /** {@inheritDoc @apollo/client!MutationResultDocumentation#client:member} */ client: ApolloClient; + /** {@inheritDoc @apollo/client!MutationResultDocumentation#reset:member} */ reset(): void; } @@ -295,12 +403,12 @@ export type MutationTuple< TContext = DefaultContext, TCache extends ApolloCache = ApolloCache, > = [ - ( + mutate: ( options?: MutationFunctionOptions // TODO This FetchResult seems strange here, as opposed to an // ApolloQueryResult ) => Promise>, - MutationResult, + result: MutationResult, ]; /* Subscription types */ @@ -319,33 +427,44 @@ export interface BaseSubscriptionOptions< TData = any, TVariables extends OperationVariables = OperationVariables, > { + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#variables:member} */ variables?: TVariables; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#fetchPolicy:member} */ fetchPolicy?: FetchPolicy; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#shouldResubscribe:member} */ shouldResubscribe?: | boolean | ((options: BaseSubscriptionOptions) => boolean); + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#client:member} */ client?: ApolloClient; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#skip:member} */ skip?: boolean; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#context:member} */ context?: DefaultContext; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onComplete:member} */ onComplete?: () => void; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onData:member} */ onData?: (options: OnDataOptions) => any; - /** - * @deprecated Use onData instead - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onSubscriptionData:member} */ onSubscriptionData?: (options: OnSubscriptionDataOptions) => any; + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onError:member} */ onError?: (error: ApolloError) => void; - /** - * @deprecated Use onComplete instead - */ + /** {@inheritDoc @apollo/client!SubscriptionOptionsDocumentation#onSubscriptionComplete:member} */ onSubscriptionComplete?: () => void; } export interface SubscriptionResult { + /** {@inheritDoc @apollo/client!SubscriptionResultDocumentation#loading:member} */ loading: boolean; + /** {@inheritDoc @apollo/client!SubscriptionResultDocumentation#data:member} */ data?: TData; + /** {@inheritDoc @apollo/client!SubscriptionResultDocumentation#error:member} */ error?: ApolloError; // This was added by the legacy useSubscription type, and is tested in unit // tests, but probably shouldn’t be added to the result. + /** + * @internal + */ variables?: TVariables; } diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index aaefb116cfb..6d4753746e5 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -19,16 +19,22 @@ import { print, } from "../../../utilities/index.js"; -export type ResultFunction = () => T; +export type ResultFunction> = (variables: V) => T; + +export type VariableMatcher> = ( + variables: V +) => boolean; export interface MockedResponse< TData = Record, TVariables = Record, > { request: GraphQLRequest; - result?: FetchResult | ResultFunction>; + maxUsageCount?: number; + result?: FetchResult | ResultFunction, TVariables>; error?: Error; delay?: number; + variableMatcher?: VariableMatcher; newData?: ResultFunction; } @@ -51,7 +57,7 @@ export class MockLink extends ApolloLink { private mockedResponsesByKey: { [key: string]: MockedResponse[] } = {}; constructor( - mockedResponses: ReadonlyArray, + mockedResponses: ReadonlyArray>, addTypename: Boolean = true, options: MockLinkOptions = Object.create(null) ) { @@ -94,6 +100,9 @@ export class MockLink extends ApolloLink { if (equal(requestVariables, mockedResponseVars)) { return true; } + if (res.variableMatcher && res.variableMatcher(operation.variables)) { + return true; + } unmatchedVars.push(mockedResponseVars); return false; }) @@ -134,11 +143,14 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} ); } } else { - mockedResponses.splice(responseIndex, 1); - + if (response.maxUsageCount && response.maxUsageCount > 1) { + response.maxUsageCount--; + } else { + mockedResponses.splice(responseIndex, 1); + } const { newData } = response; if (newData) { - response.result = newData(); + response.result = newData(operation.variables); mockedResponses.push(response); } @@ -171,7 +183,7 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} if (response.result) { observer.next( typeof response.result === "function" ? - (response.result as ResultFunction)() + response.result(operation.variables) : response.result ); } @@ -199,8 +211,34 @@ ${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} if (query) { newMockedResponse.request.query = query; } + + mockedResponse.maxUsageCount = mockedResponse.maxUsageCount ?? 1; + invariant( + mockedResponse.maxUsageCount > 0, + `Mock response maxUsageCount must be greater than 0, %s given`, + mockedResponse.maxUsageCount + ); + + this.normalizeVariableMatching(newMockedResponse); return newMockedResponse; } + + private normalizeVariableMatching(mockedResponse: MockedResponse) { + const variables = mockedResponse.request.variables; + if (mockedResponse.variableMatcher && variables) { + throw new Error( + "Mocked response should contain either variableMatcher or request.variables" + ); + } + + if (!mockedResponse.variableMatcher) { + mockedResponse.variableMatcher = (vars) => { + const requestVariables = vars || {}; + const mockedResponseVariables = variables || {}; + return equal(requestVariables, mockedResponseVariables); + }; + } + } } export interface MockApolloLink extends ApolloLink { diff --git a/src/testing/internal/ObservableStream.ts b/src/testing/internal/ObservableStream.ts index f0416692331..b19be0d469b 100644 --- a/src/testing/internal/ObservableStream.ts +++ b/src/testing/internal/ObservableStream.ts @@ -29,6 +29,7 @@ async function* observableToAsyncEventIterator(observable: Observable) { (error) => resolveNext({ type: "error", error }), () => resolveNext({ type: "complete" }) ); + yield "initialization value" as unknown as Promise>; while (true) { yield promises.shift()!; @@ -54,7 +55,11 @@ class IteratorStream { export class ObservableStream extends IteratorStream> { constructor(observable: Observable) { - super(observableToAsyncEventIterator(observable)); + const iterator = observableToAsyncEventIterator(observable); + // we need to call next() once to start the generator so we immediately subscribe. + // the first value is always "initialization value" which we don't care about + iterator.next(); + super(iterator); } async takeNext(options?: TakeOptions): Promise { diff --git a/src/testing/internal/disposables/disableActWarnings.ts b/src/testing/internal/disposables/disableActWarnings.ts new file mode 100644 index 00000000000..c5254c8dc1d --- /dev/null +++ b/src/testing/internal/disposables/disableActWarnings.ts @@ -0,0 +1,15 @@ +import { withCleanup } from "./withCleanup.js"; + +/** + * Temporarily disable act warnings. + * + * https://github.com/reactwg/react-18/discussions/102 + */ +export function disableActWarnings() { + const prev = { prevActEnv: (globalThis as any).IS_REACT_ACT_ENVIRONMENT }; + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = false; + + return withCleanup(prev, ({ prevActEnv }) => { + (globalThis as any).IS_REACT_ACT_ENVIRONMENT = prevActEnv; + }); +} diff --git a/src/testing/internal/disposables/index.ts b/src/testing/internal/disposables/index.ts index 6d232565db4..9895d129589 100644 --- a/src/testing/internal/disposables/index.ts +++ b/src/testing/internal/disposables/index.ts @@ -1,2 +1,3 @@ +export { disableActWarnings } from "./disableActWarnings.js"; export { spyOnConsole } from "./spyOnConsole.js"; export { withCleanup } from "./withCleanup.js"; diff --git a/src/testing/internal/index.ts b/src/testing/internal/index.ts index 73a9a00ff0e..9f8b3faae9a 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 { + setupSimpleCase, + setupVariablesCase, + setupPaginatedCase, +} from "./scenarios/index.js"; + +export type { + RenderWithClientOptions, + RenderWithMocksOptions, +} from "./renderHelpers.js"; +export { renderWithClient, renderWithMocks } from "./renderHelpers.js"; diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index 5bf0531ffab..0757392b26d 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -62,6 +62,8 @@ export interface Render extends BaseRender { * ``` */ withinDOM: () => SyncScreen; + + renderedComponents: Array; } /** @internal */ @@ -77,7 +79,8 @@ export class RenderInstance implements Render { constructor( baseRender: BaseRender, public snapshot: Snapshot, - private stringifiedDOM: string | undefined + private stringifiedDOM: string | undefined, + public renderedComponents: Array ) { this.id = baseRender.id; this.phase = baseRender.phase; diff --git a/src/testing/internal/profile/context.tsx b/src/testing/internal/profile/context.tsx new file mode 100644 index 00000000000..a8488e73a6c --- /dev/null +++ b/src/testing/internal/profile/context.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; + +export interface ProfilerContextValue { + renderedComponents: Array; +} + +const ProfilerContext = React.createContext( + undefined +); + +export function ProfilerContextProvider({ + children, + value, +}: { + children: React.ReactNode; + value: ProfilerContextValue; +}) { + const parentContext = useProfilerContext(); + + if (parentContext) { + throw new Error("Profilers should not be nested in the same tree"); + } + + return ( + + {children} + + ); +} + +export function useProfilerContext() { + return React.useContext(ProfilerContext); +} diff --git a/src/testing/internal/profile/index.ts b/src/testing/internal/profile/index.ts index 01bb526c52c..3d9ddd55559 100644 --- a/src/testing/internal/profile/index.ts +++ b/src/testing/internal/profile/index.ts @@ -1,8 +1,15 @@ export type { NextRenderOptions, + Profiler, ProfiledComponent, ProfiledHook, } from "./profile.js"; -export { profile, profileHook, WaitForRenderTimeoutError } from "./profile.js"; +export { + createProfiler, + profile, + profileHook, + useTrackRenders, + WaitForRenderTimeoutError, +} from "./profile.js"; export type { SyncScreen } from "./Render.js"; diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index d4e4c604b11..12e681ad7d2 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -8,6 +8,9 @@ global.TextDecoder ??= TextDecoder; import type { Render, BaseRender } from "./Render.js"; import { RenderInstance } from "./Render.js"; import { applyStackTrace, captureStackTrace } from "./traces.js"; +import type { ProfilerContextValue } from "./context.js"; +import { ProfilerContextProvider, useProfilerContext } from "./context.js"; +import { disableActWarnings } from "../disposables/index.js"; type ValidSnapshot = void | (object & { /* not a function */ call?: never }); @@ -20,10 +23,15 @@ export interface NextRenderOptions { } /** @internal */ -export interface ProfiledComponent - extends React.FC, - ProfiledComponentFields, - ProfiledComponentOnlyFields {} +interface ProfilerProps { + children: React.ReactNode; +} + +/** @internal */ +export interface Profiler + extends React.FC, + ProfiledComponentFields, + ProfiledComponentOnlyFields {} interface ReplaceSnapshot { (newSnapshot: Snapshot): void; @@ -39,13 +47,13 @@ interface MergeSnapshot { ): void; } -interface ProfiledComponentOnlyFields { +interface ProfiledComponentOnlyFields { // Allows for partial updating of the snapshot by shallow merging the results mergeSnapshot: MergeSnapshot; // Performs a full replacement of the snapshot replaceSnapshot: ReplaceSnapshot; } -interface ProfiledComponentFields { +interface ProfiledComponentFields { /** * An array of all renders that have happened so far. * Errors thrown during component render will be captured here, too. @@ -81,17 +89,50 @@ interface ProfiledComponentFields { waitForNextRender(options?: NextRenderOptions): Promise>; } +export interface ProfiledComponent + extends React.FC, + ProfiledComponentFields, + ProfiledComponentOnlyFields {} + /** @internal */ -export function profile< - Snapshot extends ValidSnapshot = void, - Props = Record, ->({ +export function profile({ Component, + ...options +}: Parameters>[0] & { + Component: React.ComponentType; +}): ProfiledComponent { + const Profiler = createProfiler(options); + + return Object.assign( + function ProfiledComponent(props: Props) { + return ( + + + + ); + }, + { + mergeSnapshot: Profiler.mergeSnapshot, + replaceSnapshot: Profiler.replaceSnapshot, + getCurrentRender: Profiler.getCurrentRender, + peekRender: Profiler.peekRender, + takeRender: Profiler.takeRender, + totalRenderCount: Profiler.totalRenderCount, + waitForNextRender: Profiler.waitForNextRender, + get renders() { + return Profiler.renders; + }, + } + ); +} + +/** @internal */ +export function createProfiler({ onRender, snapshotDOM = false, initialSnapshot, + skipNonTrackingRenders, }: { - Component: React.ComponentType; onRender?: ( info: BaseRender & { snapshot: Snapshot; @@ -101,7 +142,12 @@ export function profile< ) => void; snapshotDOM?: boolean; initialSnapshot?: Snapshot; -}) { + /** + * This will skip renders during which no renders tracked by + * `useTrackRenders` occured. + */ + skipNonTrackingRenders?: boolean; +} = {}) { let nextRender: Promise> | undefined; let resolveNextRender: ((render: Render) => void) | undefined; let rejectNextRender: ((error: unknown) => void) | undefined; @@ -133,6 +179,10 @@ export function profile< })); }; + const profilerContext: ProfilerContextValue = { + renderedComponents: [], + }; + const profilerOnRender: React.ProfilerOnRenderCallback = ( id, phase, @@ -141,6 +191,12 @@ export function profile< startTime, commitTime ) => { + if ( + skipNonTrackingRenders && + profilerContext.renderedComponents.length === 0 + ) { + return; + } const baseRender = { id, phase, @@ -148,7 +204,7 @@ export function profile< baseDuration, startTime, commitTime, - count: Profiled.renders.length + 1, + count: Profiler.renders.length + 1, }; try { /* @@ -168,13 +224,19 @@ export function profile< const snapshot = snapshotRef.current as Snapshot; const domSnapshot = snapshotDOM ? window.document.body.innerHTML : undefined; - const render = new RenderInstance(baseRender, snapshot, domSnapshot); - Profiled.renders.push(render); + const render = new RenderInstance( + baseRender, + snapshot, + domSnapshot, + profilerContext.renderedComponents + ); + profilerContext.renderedComponents = []; + Profiler.renders.push(render); resolveNextRender?.(render); } catch (error) { - Profiled.renders.push({ + Profiler.renders.push({ phase: "snapshotError", - count: Profiled.renders.length, + count: Profiler.renders.length, error, }); rejectNextRender?.(error); @@ -184,27 +246,31 @@ export function profile< }; let iteratorPosition = 0; - const Profiled: ProfiledComponent = Object.assign( - (props: Props) => ( - - - - ), + const Profiler: Profiler = Object.assign( + ({ children }: ProfilerProps) => { + return ( + + + {children} + + + ); + }, { replaceSnapshot, mergeSnapshot, - } satisfies ProfiledComponentOnlyFields, + } satisfies ProfiledComponentOnlyFields, { renders: new Array< | Render | { phase: "snapshotError"; count: number; error: unknown } >(), totalRenderCount() { - return Profiled.renders.length; + return Profiler.renders.length; }, async peekRender(options: NextRenderOptions = {}) { - if (iteratorPosition < Profiled.renders.length) { - const render = Profiled.renders[iteratorPosition]; + if (iteratorPosition < Profiler.renders.length) { + const render = Profiler.renders[iteratorPosition]; if (render.phase === "snapshotError") { throw render.error; @@ -212,16 +278,22 @@ export function profile< return render; } - return Profiled.waitForNextRender({ - [_stackTrace]: captureStackTrace(Profiled.peekRender), + return Profiler.waitForNextRender({ + [_stackTrace]: captureStackTrace(Profiler.peekRender), ...options, }); }, async takeRender(options: NextRenderOptions = {}) { + // In many cases we do not control the resolution of the suspended + // promise which results in noisy tests when the profiler due to + // repeated act warnings. + using _disabledActWarnings = disableActWarnings(); + let error: unknown = undefined; + try { - return await Profiled.peekRender({ - [_stackTrace]: captureStackTrace(Profiled.takeRender), + return await Profiler.peekRender({ + [_stackTrace]: captureStackTrace(Profiler.takeRender), ...options, }); } catch (e) { @@ -247,7 +319,7 @@ export function profile< ); } - const render = Profiled.renders[currentPosition]; + const render = Profiler.renders[currentPosition]; if (render.phase === "snapshotError") { throw render.error; @@ -258,7 +330,7 @@ export function profile< timeout = 1000, // capture the stack trace here so its stack trace is as close to the calling code as possible [_stackTrace]: stackTrace = captureStackTrace( - Profiled.waitForNextRender + Profiler.waitForNextRender ), }: NextRenderOptions = {}) { if (!nextRender) { @@ -280,9 +352,9 @@ export function profile< } return nextRender; }, - } satisfies ProfiledComponentFields + } satisfies ProfiledComponentFields ); - return Profiled; + return Profiler; } /** @internal */ @@ -303,8 +375,8 @@ type ResultReplaceRenderWithSnapshot = (...args: Args) => Promise : T; -type ProfiledHookFields = - ProfiledComponentFields extends infer PC ? +type ProfiledHookFields = + ProfiledComponentFields extends infer PC ? { [K in keyof PC as StringReplaceRenderWithSnapshot< K & string @@ -315,45 +387,74 @@ type ProfiledHookFields = /** @internal */ export interface ProfiledHook extends React.FC, - ProfiledHookFields { - ProfiledComponent: ProfiledComponent; + ProfiledHookFields { + Profiler: Profiler; } /** @internal */ export function profileHook( renderCallback: (props: Props) => ReturnValue ): ProfiledHook { - let returnValue: ReturnValue; - const Component = (props: Props) => { - ProfiledComponent.replaceSnapshot(renderCallback(props)); + const Profiler = createProfiler(); + + const ProfiledHook = (props: Props) => { + Profiler.replaceSnapshot(renderCallback(props)); return null; }; - const ProfiledComponent = profile({ - Component, - onRender: () => returnValue, - }); + return Object.assign( - function ProfiledHook(props: Props) { - return ; + function App(props: Props) { + return ( + + + + ); }, { - ProfiledComponent, + Profiler, }, { - renders: ProfiledComponent.renders, - totalSnapshotCount: ProfiledComponent.totalRenderCount, + renders: Profiler.renders, + totalSnapshotCount: Profiler.totalRenderCount, async peekSnapshot(options) { - return (await ProfiledComponent.peekRender(options)).snapshot; + return (await Profiler.peekRender(options)).snapshot; }, async takeSnapshot(options) { - return (await ProfiledComponent.takeRender(options)).snapshot; + return (await Profiler.takeRender(options)).snapshot; }, getCurrentSnapshot() { - return ProfiledComponent.getCurrentRender().snapshot; + return Profiler.getCurrentRender().snapshot; }, async waitForNextSnapshot(options) { - return (await ProfiledComponent.waitForNextRender(options)).snapshot; + return (await Profiler.waitForNextRender(options)).snapshot; }, - } satisfies ProfiledHookFields + } satisfies ProfiledHookFields ); } + +function resolveHookOwner(): React.ComponentType | undefined { + return (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED + ?.ReactCurrentOwner?.current?.elementType; +} + +export function useTrackRenders({ name }: { name?: string } = {}) { + const component = name || resolveHookOwner(); + + if (!component) { + throw new Error( + "useTrackRender: Unable to determine component. Please ensure the hook is called inside a rendered component or provide a `name` option." + ); + } + + const ctx = useProfilerContext(); + + if (!ctx) { + throw new Error( + "useTrackComponentRender: A Profiler must be created and rendered to track component renders" + ); + } + + React.useLayoutEffect(() => { + ctx.renderedComponents.unshift(component); + }); +} 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..637c2a7bec4 --- /dev/null +++ b/src/testing/internal/scenarios/index.ts @@ -0,0 +1,110 @@ +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 setupSimpleCase() { + 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: { + __typename: "Character"; + id: string; + name: string; + }; +} + +export interface VariablesCaseVariables { + id: string; +} + +export function setupVariablesCase() { + 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: { __typename: "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 setupPaginatedCase() { + const query: TypedDocumentNode = + gql` + query letters($limit: Int, $offset: Int) { + letters(limit: $limit) { + letter + position + } + } + `; + + const data = "ABCDEFGHIJKLMNOPQRSTUV".split("").map((letter, index) => ({ + __typename: "Letter", + 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/ProfiledComponent.ts b/src/testing/matchers/ProfiledComponent.ts index afab9aec343..435c24a29a6 100644 --- a/src/testing/matchers/ProfiledComponent.ts +++ b/src/testing/matchers/ProfiledComponent.ts @@ -2,22 +2,22 @@ import type { MatcherFunction } from "expect"; import { WaitForRenderTimeoutError } from "../internal/index.js"; import type { NextRenderOptions, + Profiler, ProfiledComponent, ProfiledHook, } from "../internal/index.js"; + export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = async function (actual, options) { - const _profiled = actual as + const _profiler = actual as + | Profiler | ProfiledComponent | ProfiledHook; - const profiled = - "ProfiledComponent" in _profiled ? - _profiled.ProfiledComponent - : _profiled; - const hint = this.utils.matcherHint("toRerender"); + const profiler = "Profiler" in _profiler ? _profiler.Profiler : _profiler; + const hint = this.utils.matcherHint("toRerender", "ProfiledComponent", ""); let pass = true; try { - await profiled.peekRender({ timeout: 100, ...options }); + await profiler.peekRender({ timeout: 100, ...options }); } catch (e) { if (e instanceof WaitForRenderTimeoutError) { pass = false; @@ -25,12 +25,13 @@ export const toRerender: MatcherFunction<[options?: NextRenderOptions]> = throw e; } } + return { pass, message() { return ( hint + - ` Expected component to${pass ? " not" : ""} rerender, ` + + `\n\nExpected component to${pass ? " not" : ""} rerender, ` + `but it did${pass ? "" : " not"}.` ); }, @@ -43,28 +44,28 @@ const failed = {}; export const toRenderExactlyTimes: MatcherFunction< [times: number, options?: NextRenderOptions] > = async function (actual, times, optionsPerRender) { - const _profiled = actual as + const _profiler = actual as + | Profiler | ProfiledComponent | ProfiledHook; - const profiled = - "ProfiledComponent" in _profiled ? _profiled.ProfiledComponent : _profiled; + const profiler = "Profiler" in _profiler ? _profiler.Profiler : _profiler; const options = { timeout: 100, ...optionsPerRender }; const hint = this.utils.matcherHint("toRenderExactlyTimes"); let pass = true; try { - if (profiled.totalRenderCount() > times) { + if (profiler.totalRenderCount() > times) { throw failed; } try { - while (profiled.totalRenderCount() < times) { - await profiled.waitForNextRender(options); + while (profiler.totalRenderCount() < times) { + await profiler.waitForNextRender(options); } } catch (e) { // timeouts here should just fail the test, rethrow other errors throw e instanceof WaitForRenderTimeoutError ? failed : e; } try { - await profiled.waitForNextRender(options); + await profiler.waitForNextRender(options); } catch (e) { // we are expecting a timeout here, so swallow that error, rethrow others if (!(e instanceof WaitForRenderTimeoutError)) { @@ -84,7 +85,7 @@ export const toRenderExactlyTimes: MatcherFunction< return ( hint + ` Expected component to${pass ? " not" : ""} render exactly ${times}.` + - ` It rendered ${profiled.totalRenderCount()} times.` + ` It rendered ${profiler.totalRenderCount()} times.` ); }, }; diff --git a/src/testing/matchers/index.d.ts b/src/testing/matchers/index.d.ts index 4df0279d871..b73b33cfd08 100644 --- a/src/testing/matchers/index.d.ts +++ b/src/testing/matchers/index.d.ts @@ -3,13 +3,20 @@ import type { DocumentNode, OperationVariables, } from "../../core/index.js"; +import type { QueryReference } from "../../react/index.js"; import { NextRenderOptions, + Profiler, ProfiledComponent, ProfiledHook, } 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`. @@ -29,15 +36,20 @@ interface ApolloCustomMatchers { ) => R : { error: "matcher needs to be called on an ApolloClient instance" }; - toRerender: T extends ProfiledComponent | ProfiledHook ? + toRerender: T extends ( + Profiler | ProfiledComponent | ProfiledHook + ) ? (options?: NextRenderOptions) => Promise : { error: "matcher needs to be called on a ProfiledComponent instance" }; toRenderExactlyTimes: T extends ( - ProfiledComponent | ProfiledHook + Profiler | ProfiledComponent | ProfiledHook ) ? (count: number, options?: NextRenderOptions) => Promise : { error: "matcher needs to be called on a ProfiledComponent instance" }; + + toBeGarbageCollected: T extends WeakRef ? () => Promise + : { error: "matcher needs to be called on a WeakRef instance" }; } declare global { diff --git a/src/testing/matchers/index.ts b/src/testing/matchers/index.ts index d2ebd8ce7c2..c4f88544f14 100644 --- a/src/testing/matchers/index.ts +++ b/src/testing/matchers/index.ts @@ -2,10 +2,14 @@ import { expect } from "@jest/globals"; import { toMatchDocument } from "./toMatchDocument.js"; import { toHaveSuspenseCacheEntryUsing } from "./toHaveSuspenseCacheEntryUsing.js"; import { toRerender, toRenderExactlyTimes } from "./ProfiledComponent.js"; +import { toBeGarbageCollected } from "./toBeGarbageCollected.js"; +import { toBeDisposed } from "./toBeDisposed.js"; expect.extend({ + toBeDisposed, toHaveSuspenseCacheEntryUsing, toMatchDocument, toRerender, toRenderExactlyTimes, + toBeGarbageCollected, }); diff --git a/src/testing/matchers/toBeDisposed.ts b/src/testing/matchers/toBeDisposed.ts new file mode 100644 index 00000000000..b9ca3c6199c --- /dev/null +++ b/src/testing/matchers/toBeDisposed.ts @@ -0,0 +1,35 @@ +import type { MatcherFunction } from "expect"; +import type { QueryReference } from "../../react/internal/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/toBeGarbageCollected.ts b/src/testing/matchers/toBeGarbageCollected.ts new file mode 100644 index 00000000000..fda58543b38 --- /dev/null +++ b/src/testing/matchers/toBeGarbageCollected.ts @@ -0,0 +1,59 @@ +import type { MatcherFunction } from "expect"; + +// this is necessary because this file is picked up by `tsc` (it's not a test), +// but our main `tsconfig.json` doesn't include `"ES2021.WeakRef"` on purpose +declare class WeakRef { + constructor(target: T); + deref(): T | undefined; +} + +export const toBeGarbageCollected: MatcherFunction<[weakRef: WeakRef]> = + async function (actual) { + const hint = this.utils.matcherHint("toBeGarbageCollected"); + + if (!(actual instanceof WeakRef)) { + throw new Error( + hint + + "\n\n" + + `Expected value to be a WeakRef, but it was a ${typeof actual}.` + ); + } + + let pass = false; + let interval: NodeJS.Timeout | undefined; + let timeout: NodeJS.Timeout | undefined; + await Promise.race([ + new Promise((resolve) => { + timeout = setTimeout(resolve, 1000); + }), + new Promise((resolve) => { + interval = setInterval(() => { + global.gc!(); + pass = actual.deref() === undefined; + if (pass) { + resolve(); + } + }, 1); + }), + ]); + + clearInterval(interval); + clearTimeout(timeout); + + return { + pass, + message: () => { + if (pass) { + return ( + hint + + "\n\n" + + "Expected value to not be cache-collected, but it was." + ); + } + + return ( + hint + "\n\n Expected value to be cache-collected, but it was not." + ); + }, + }; + }; diff --git a/src/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< [ diff --git a/src/testing/react/MockedProvider.tsx b/src/testing/react/MockedProvider.tsx index cc215fe3d5d..fa06db3c324 100644 --- a/src/testing/react/MockedProvider.tsx +++ b/src/testing/react/MockedProvider.tsx @@ -11,7 +11,7 @@ import type { Resolvers } from "../../core/index.js"; import type { ApolloCache } from "../../cache/index.js"; export interface MockedProviderProps { - mocks?: ReadonlyArray; + mocks?: ReadonlyArray>; addTypename?: boolean; defaultOptions?: DefaultOptions; cache?: ApolloCache; diff --git a/src/testing/react/__tests__/MockedProvider.test.tsx b/src/testing/react/__tests__/MockedProvider.test.tsx index 610cc0f2124..4ebd0878a43 100644 --- a/src/testing/react/__tests__/MockedProvider.test.tsx +++ b/src/testing/react/__tests__/MockedProvider.test.tsx @@ -1,14 +1,15 @@ import React from "react"; import { DocumentNode } from "graphql"; -import { render, screen, waitFor } from "@testing-library/react"; +import { act, render, screen, waitFor } from "@testing-library/react"; import gql from "graphql-tag"; import { itAsync, MockedResponse, MockLink } from "../../core"; import { MockedProvider } from "../MockedProvider"; import { useQuery } from "../../../react/hooks"; import { InMemoryCache } from "../../../cache"; -import { ApolloLink } from "../../../link/core"; -import { spyOnConsole } from "../../internal"; +import { QueryResult } from "../../../react/types/types"; +import { ApolloLink, FetchResult } from "../../../link/core"; +import { Observable } from "zen-observable-ts"; const variables = { username: "mock_username", @@ -56,13 +57,17 @@ interface Data { }; } +interface Result { + current: QueryResult | null; +} + interface Variables { username: string; } let errorThrown = false; const errorLink = new ApolloLink((operation, forward) => { - let observer = null; + let observer: Observable | null = null; try { observer = forward(operation); } catch (error) { @@ -98,6 +103,100 @@ describe("General use", () => { }).then(resolve, reject); }); + itAsync( + "should pass the variables to the result function", + async (resolve, reject) => { + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; + } + + const mock2: MockedResponse = { + request: { + query, + variables, + }, + result: jest.fn().mockResolvedValue({ data: { user } }), + }; + + render( + + + + ); + + waitFor(() => { + expect(mock2.result as jest.Mock).toHaveBeenCalledWith(variables); + }).then(resolve, reject); + } + ); + + itAsync( + "should pass the variables to the variableMatcher", + async (resolve, reject) => { + function Component({ ...variables }: Variables) { + useQuery(query, { variables }); + return null; + } + + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: jest.fn().mockReturnValue(true), + result: { data: { user } }, + }; + + render( + + + + ); + + waitFor(() => { + expect(mock2.variableMatcher as jest.Mock).toHaveBeenCalledWith( + variables + ); + }).then(resolve, reject); + } + ); + + itAsync( + "should use a mock if the variableMatcher returns true", + async (resolve, reject) => { + let finished = false; + + function Component({ username }: Variables) { + const { loading, data } = useQuery(query, { + variables, + }); + if (!loading) { + expect(data!.user).toMatchSnapshot(); + finished = true; + } + return null; + } + + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: (v) => v.username === variables.username, + result: { data: { user } }, + }; + + render( + + + + ); + + waitFor(() => { + expect(finished).toBe(true); + }).then(resolve, reject); + } + ); + itAsync("should allow querying with the typename", (resolve, reject) => { let finished = false; function Component({ username }: Variables) { @@ -191,6 +290,41 @@ describe("General use", () => { } ); + itAsync( + "should error if the variableMatcher returns false", + async (resolve, reject) => { + let finished = false; + function Component({ ...variables }: Variables) { + const { loading, error } = useQuery(query, { + variables, + }); + if (!loading) { + expect(error).toMatchSnapshot(); + finished = true; + } + return null; + } + + const mock2: MockedResponse = { + request: { + query, + }, + variableMatcher: () => false, + result: { data: { user } }, + }; + + render( + + + + ); + + waitFor(() => { + expect(finished).toBe(true); + }).then(resolve, reject); + } + ); + itAsync( "should error if the variables do not deep equal", (resolve, reject) => { @@ -482,6 +616,243 @@ describe("General use", () => { expect(errorThrown).toBeFalsy(); }); + it("Uses a mock a configured number of times when `maxUsageCount` is configured", async () => { + const result: Result = { current: null }; + function Component({ username }: Variables) { + result.current = useQuery(query, { + variables: { username }, + }); + return null; + } + + const waitForLoaded = async () => { + await waitFor(() => { + expect(result.current?.loading).toBe(false); + expect(result.current?.error).toBeUndefined(); + }); + }; + + const waitForError = async () => { + await waitFor(() => { + expect(result.current?.error?.message).toMatch( + /No more mocked responses/ + ); + }); + }; + + const refetch = () => { + return act(async () => { + try { + await result.current?.refetch(); + } catch {} + }); + }; + + const mocks: ReadonlyArray = [ + { + request: { + query, + variables: { + username: "mock_username", + }, + }, + maxUsageCount: 2, + result: { data: { user } }, + }, + ]; + + const mockLink = new MockLink(mocks, true, { showWarnings: false }); + const link = ApolloLink.from([errorLink, mockLink]); + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + render(, { wrapper: Wrapper }); + await waitForLoaded(); + await refetch(); + await waitForLoaded(); + await refetch(); + await waitForError(); + }); + + it("Uses a mock infinite number of times when `maxUsageCount` is configured with Number.POSITIVE_INFINITY", async () => { + const result: Result = { current: null }; + function Component({ username }: Variables) { + result.current = useQuery(query, { + variables: { username }, + }); + return null; + } + + const waitForLoaded = async () => { + await waitFor(() => { + expect(result.current?.loading).toBe(false); + expect(result.current?.error).toBeUndefined(); + }); + }; + + const refetch = () => { + return act(async () => { + try { + await result.current?.refetch(); + } catch {} + }); + }; + + const mocks: ReadonlyArray = [ + { + request: { + query, + variables: { + username: "mock_username", + }, + }, + maxUsageCount: Number.POSITIVE_INFINITY, + result: { data: { user } }, + }, + ]; + + const mockLink = new MockLink(mocks, true, { showWarnings: false }); + const link = ApolloLink.from([errorLink, mockLink]); + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + render(, { wrapper: Wrapper }); + for (let i = 0; i < 100; i++) { + await waitForLoaded(); + await refetch(); + } + await waitForLoaded(); + }); + + it("uses a mock once when `maxUsageCount` is not configured", async () => { + const result: Result = { current: null }; + function Component({ username }: Variables) { + result.current = useQuery(query, { + variables: { username }, + }); + return null; + } + + const waitForLoaded = async () => { + await waitFor(() => { + expect(result.current?.loading).toBe(false); + expect(result.current?.error).toBeUndefined(); + }); + }; + + const waitForError = async () => { + await waitFor(() => { + expect(result.current?.error?.message).toMatch( + /No more mocked responses/ + ); + }); + }; + + const refetch = () => { + return act(async () => { + try { + await result.current?.refetch(); + } catch {} + }); + }; + + const mocks: ReadonlyArray = [ + { + request: { + query, + variables: { + username: "mock_username", + }, + }, + result: { data: { user } }, + }, + ]; + + const mockLink = new MockLink(mocks, true, { showWarnings: false }); + const link = ApolloLink.from([errorLink, mockLink]); + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + render(, { wrapper: Wrapper }); + await waitForLoaded(); + await refetch(); + await waitForError(); + }); + + it("can still use other mocks after a mock has been fully consumed", async () => { + const result: Result = { current: null }; + function Component({ username }: Variables) { + result.current = useQuery(query, { + variables: { username }, + }); + return null; + } + + const waitForLoaded = async () => { + await waitFor(() => { + expect(result.current?.loading).toBe(false); + expect(result.current?.error).toBeUndefined(); + }); + }; + + const refetch = () => { + return act(async () => { + try { + await result.current?.refetch(); + } catch {} + }); + }; + + const mocks: ReadonlyArray = [ + { + request: { + query, + variables: { + username: "mock_username", + }, + }, + maxUsageCount: 2, + result: { data: { user } }, + }, + { + request: { + query, + variables: { + username: "mock_username", + }, + }, + result: { + data: { + user: { + __typename: "User", + id: "new_id", + }, + }, + }, + }, + ]; + + const mockLink = new MockLink(mocks, true, { showWarnings: false }); + const link = ApolloLink.from([errorLink, mockLink]); + const Wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + + render(, { wrapper: Wrapper }); + await waitForLoaded(); + await refetch(); + await waitForLoaded(); + await refetch(); + await waitForLoaded(); + expect(result.current?.data?.user).toEqual({ + __typename: "User", + id: "new_id", + }); + }); + it('should return "Mocked response should contain" errors in response', async () => { let finished = false; function Component({ ...variables }: Variables) { @@ -522,7 +893,7 @@ describe("General use", () => { }); it("shows a warning in the console when there is no matched mock", async () => { - using _consoleSpy = spyOnConsole("warn"); + const consoleSpy = jest.spyOn(console, "warn").mockImplementation(() => {}); let finished = false; function Component({ ...variables }: Variables) { const { loading } = useQuery(query, { variables }); @@ -562,10 +933,12 @@ describe("General use", () => { expect(console.warn).toHaveBeenCalledWith( expect.stringContaining("No more mocked responses for the query") ); + + consoleSpy.mockRestore(); }); it("silences console warning for unmatched mocks when `showWarnings` is `false`", async () => { - using _consoleSpy = spyOnConsole("warn"); + const consoleSpy = jest.spyOn(console, "warn"); let finished = false; function Component({ ...variables }: Variables) { const { loading } = useQuery(query, { variables }); @@ -602,10 +975,12 @@ describe("General use", () => { }); expect(console.warn).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); }); it("silences console warning for unmatched mocks when passing `showWarnings` to `MockLink` directly", async () => { - using _consoleSpy = spyOnConsole("warn"); + const consoleSpy = jest.spyOn(console, "warn"); let finished = false; function Component({ ...variables }: Variables) { const { loading } = useQuery(query, { variables }); @@ -646,6 +1021,8 @@ describe("General use", () => { }); expect(console.warn).not.toHaveBeenCalled(); + + consoleSpy.mockRestore(); }); itAsync( diff --git a/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap b/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap index 5fecc4e98d7..727f5edbb85 100644 --- a/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap +++ b/src/testing/react/__tests__/__snapshots__/MockedProvider.test.tsx.snap @@ -18,6 +18,20 @@ Expected variables: {"username":"mock_username"} ] `; +exports[`General use should error if the variableMatcher returns false 1`] = ` +[ApolloError: No more mocked responses for the query: query GetUser($username: String!) { + user(username: $username) { + id + __typename + } +} +Expected variables: {"username":"mock_username"} + +Failed to match 1 mock for this query. The mocked response had the following variables: + {} +] +`; + exports[`General use should error if the variables do not deep equal 1`] = ` [ApolloError: No more mocked responses for the query: query GetUser($username: String!) { user(username: $username) { @@ -87,3 +101,10 @@ exports[`General use should support custom error handling using setOnError 1`] = Expected variables: {"username":"mock_username"} ] `; + +exports[`General use should use a mock if the variableMatcher returns true 1`] = ` +Object { + "__typename": "User", + "id": "user_id", +} +`; diff --git a/src/tsconfig.json b/src/tsconfig.json index e5f75afe0f6..efeb2f2da38 100644 --- a/src/tsconfig.json +++ b/src/tsconfig.json @@ -5,7 +5,7 @@ { "compilerOptions": { "noEmit": true, - "lib": ["es2015", "esnext.asynciterable"], + "lib": ["es2015", "esnext.asynciterable", "ES2021.WeakRef"], "types": ["jest", "node", "./testing/matchers/index.d.ts"] }, "extends": "../tsconfig.json", diff --git a/src/utilities/caching/__tests__/getMemoryInternals.ts b/src/utilities/caching/__tests__/getMemoryInternals.ts new file mode 100644 index 00000000000..c83f34da27f --- /dev/null +++ b/src/utilities/caching/__tests__/getMemoryInternals.ts @@ -0,0 +1,204 @@ +import { createFragmentRegistry } from "../../../cache"; +import { + ApolloClient, + ApolloLink, + DocumentTransform, + InMemoryCache, + gql, +} from "../../../core"; +import { createPersistedQueryLink } from "../../../link/persisted-queries"; +import { removeTypenameFromVariables } from "../../../link/remove-typename"; +import crypto from "crypto"; +// importing react so the `parser` cache initializes +import "../../../react"; +import { cacheSizes, defaultCacheSizes } from "../sizes"; + +function sha256(data: string) { + const hash = crypto.createHash("sha256"); + hash.update(data); + return hash.digest("hex"); +} + +const defaultCacheSizesAsObject = { + parser: defaultCacheSizes["parser"], + canonicalStringify: defaultCacheSizes["canonicalStringify"], + print: defaultCacheSizes["print"], + "documentTransform.cache": defaultCacheSizes["documentTransform.cache"], + "queryManager.getDocumentInfo": + defaultCacheSizes["queryManager.getDocumentInfo"], + "PersistedQueryLink.persistedQueryHashes": + defaultCacheSizes["PersistedQueryLink.persistedQueryHashes"], + "fragmentRegistry.transform": defaultCacheSizes["fragmentRegistry.transform"], + "fragmentRegistry.lookup": defaultCacheSizes["fragmentRegistry.lookup"], + "fragmentRegistry.findFragmentSpreads": + defaultCacheSizes["fragmentRegistry.findFragmentSpreads"], + "cache.fragmentQueryDocuments": + defaultCacheSizes["cache.fragmentQueryDocuments"], + "removeTypenameFromVariables.getVariableDefinitions": + defaultCacheSizes["removeTypenameFromVariables.getVariableDefinitions"], + "inMemoryCache.maybeBroadcastWatch": + defaultCacheSizes["inMemoryCache.maybeBroadcastWatch"], + "inMemoryCache.executeSelectionSet": + defaultCacheSizes["inMemoryCache.executeSelectionSet"], + "inMemoryCache.executeSubSelectedArray": + defaultCacheSizes["inMemoryCache.executeSubSelectedArray"], +}; + +it("returns information about cache usage (empty caches)", () => { + const client = new ApolloClient({ + documentTransform: new DocumentTransform((x) => x, { + cache: true, + }).concat( + new DocumentTransform((x) => x, { + cache: true, + }) + ), + cache: new InMemoryCache({ + fragments: createFragmentRegistry(), + }), + link: createPersistedQueryLink({ + sha256, + }) + .concat(removeTypenameFromVariables()) + .concat(ApolloLink.empty()), + }); + expect(client.getMemoryInternals?.()).toEqual({ + limits: defaultCacheSizesAsObject, + sizes: { + parser: 0, + canonicalStringify: 0, + print: 0, + addTypenameDocumentTransform: [ + { + cache: 0, + }, + ], + queryManager: { + getDocumentInfo: 0, + documentTransforms: [ + { + cache: 0, + }, + { + cache: 0, + }, + ], + }, + fragmentRegistry: { + findFragmentSpreads: 0, + lookup: 0, + transform: 0, + }, + cache: { + fragmentQueryDocuments: 0, + }, + inMemoryCache: { + executeSelectionSet: 0, + executeSubSelectedArray: 0, + maybeBroadcastWatch: 0, + }, + links: [ + { + PersistedQueryLink: { + persistedQueryHashes: 0, + }, + }, + { + removeTypenameFromVariables: { + getVariableDefinitions: 0, + }, + }, + ], + }, + }); +}); + +it("returns information about cache usage (some query triggered)", () => { + const client = new ApolloClient({ + documentTransform: new DocumentTransform((x) => x, { + cache: true, + }).concat( + new DocumentTransform((x) => x, { + cache: true, + }) + ), + cache: new InMemoryCache({ + fragments: createFragmentRegistry(), + }), + link: createPersistedQueryLink({ + sha256, + }) + .concat(removeTypenameFromVariables()) + .concat(ApolloLink.empty()), + }); + + client.query({ + query: gql` + query { + hello + } + `, + }); + expect(client.getMemoryInternals?.()).toStrictEqual({ + limits: defaultCacheSizesAsObject, + sizes: { + parser: 0, + canonicalStringify: 0, + print: 1, + addTypenameDocumentTransform: [ + { + cache: 1, + }, + ], + queryManager: { + getDocumentInfo: 1, + documentTransforms: [ + { + cache: 1, + }, + { + cache: 1, + }, + ], + }, + fragmentRegistry: { + findFragmentSpreads: 1, + lookup: 0, + transform: 1, + }, + cache: { + fragmentQueryDocuments: 0, + }, + inMemoryCache: { + executeSelectionSet: 1, + executeSubSelectedArray: 0, + maybeBroadcastWatch: 0, + }, + links: [ + { + PersistedQueryLink: { + persistedQueryHashes: 1, + }, + }, + { + removeTypenameFromVariables: { + getVariableDefinitions: 0, + }, + }, + ], + }, + }); +}); + +it("reports user-declared cacheSizes", () => { + const client = new ApolloClient({ + cache: new InMemoryCache({}), + }); + + cacheSizes["inMemoryCache.executeSubSelectedArray"] = 90; + + expect(client.getMemoryInternals?.().limits).toStrictEqual({ + ...defaultCacheSizesAsObject, + "inMemoryCache.executeSubSelectedArray": 90, + }); +}); diff --git a/src/utilities/caching/__tests__/sizes.test.ts b/src/utilities/caching/__tests__/sizes.test.ts new file mode 100644 index 00000000000..8339c8950f3 --- /dev/null +++ b/src/utilities/caching/__tests__/sizes.test.ts @@ -0,0 +1,11 @@ +import { expectTypeOf } from "expect-type"; +import type { CacheSizes, defaultCacheSizes } from "../sizes"; + +test.skip("type tests", () => { + expectTypeOf().toMatchTypeOf< + keyof typeof defaultCacheSizes + >(); + expectTypeOf().toMatchTypeOf< + keyof CacheSizes + >(); +}); diff --git a/src/utilities/caching/caches.ts b/src/utilities/caching/caches.ts new file mode 100644 index 00000000000..6acbdb88d34 --- /dev/null +++ b/src/utilities/caching/caches.ts @@ -0,0 +1,80 @@ +import type { CommonCache } from "@wry/caches"; +import { WeakCache, StrongCache } from "@wry/caches"; + +const scheduledCleanup = new WeakSet>(); +function schedule(cache: CommonCache) { + if (!scheduledCleanup.has(cache)) { + scheduledCleanup.add(cache); + setTimeout(() => { + cache.clean(); + scheduledCleanup.delete(cache); + }, 100); + } +} +/** + * @internal + * A version of WeakCache that will auto-schedule a cleanup of the cache when + * a new item is added. + * Throttled to once per 100ms. + * + * @privateRemarks + * Should be used throughout the rest of the codebase instead of WeakCache, + * with the notable exception of usage in `wrap` from `optimism` - that one + * already handles cleanup and should remain a `WeakCache`. + */ +export const AutoCleanedWeakCache = function ( + max?: number | undefined, + dispose?: ((value: any, key: any) => void) | undefined +) { + /* + Some builds of `WeakCache` are function prototypes, some are classes. + This library still builds with an ES5 target, so we can't extend the + real classes. + Instead, we have to use this workaround until we switch to a newer build + target. + */ + const cache = new WeakCache(max, dispose); + cache.set = function (key: any, value: any) { + schedule(this); + return WeakCache.prototype.set.call(this, key, value); + }; + return cache; +} as any as typeof WeakCache; +/** + * @internal + */ +export type AutoCleanedWeakCache = WeakCache; + +/** + * @internal + * A version of StrongCache that will auto-schedule a cleanup of the cache when + * a new item is added. + * Throttled to once per 100ms. + * + * @privateRemarks + * Should be used throughout the rest of the codebase instead of StrongCache, + * with the notable exception of usage in `wrap` from `optimism` - that one + * already handles cleanup and should remain a `StrongCache`. + */ +export const AutoCleanedStrongCache = function ( + max?: number | undefined, + dispose?: ((value: any, key: any) => void) | undefined +) { + /* + Some builds of `StrongCache` are function prototypes, some are classes. + This library still builds with an ES5 target, so we can't extend the + real classes. + Instead, we have to use this workaround until we switch to a newer build + target. + */ + const cache = new StrongCache(max, dispose); + cache.set = function (key: any, value: any) { + schedule(this); + return StrongCache.prototype.set.call(this, key, value); + }; + return cache; +} as any as typeof StrongCache; +/** + * @internal + */ +export type AutoCleanedStrongCache = StrongCache; diff --git a/src/utilities/caching/getMemoryInternals.ts b/src/utilities/caching/getMemoryInternals.ts new file mode 100644 index 00000000000..ac28989c37b --- /dev/null +++ b/src/utilities/caching/getMemoryInternals.ts @@ -0,0 +1,228 @@ +import type { OptimisticWrapperFunction } from "optimism"; +import type { + InMemoryCache, + DocumentTransform, + ApolloLink, + ApolloCache, +} from "../../core/index.js"; +import type { ApolloClient } from "../../core/index.js"; +import type { CacheSizes } from "./sizes.js"; +import { cacheSizes, defaultCacheSizes } from "./sizes.js"; + +const globalCaches: { + print?: () => number; + parser?: () => number; + canonicalStringify?: () => number; +} = {}; + +export function registerGlobalCache( + name: keyof typeof globalCaches, + getSize: () => number +) { + globalCaches[name] = getSize; +} + +/** + * Transformative helper type to turn a function of the form + * ```ts + * (this: any) => R + * ``` + * into a function of the form + * ```ts + * () => R + * ``` + * preserving the return type, but removing the `this` parameter. + * + * @remarks + * + * Further down in the definitions of `_getApolloClientMemoryInternals`, + * `_getApolloCacheMemoryInternals` and `_getInMemoryCacheMemoryInternals`, + * having the `this` parameter annotation is extremely useful for type checking + * inside the function. + * + * If this is preserved in the exported types, though, it leads to a situation + * where `ApolloCache.getMemoryInternals` is a function that requires a `this` + * of the type `ApolloCache`, while the extending class `InMemoryCache` has a + * `getMemoryInternals` function that requires a `this` of the type + * `InMemoryCache`. + * This is not compatible with TypeScript's inheritence system (although it is + * perfectly correct), and so TypeScript will complain loudly. + * + * We still want to define our functions with the `this` annotation, though, + * and have the return type inferred. + * (This requirement for return type inference here makes it impossible to use + * a function overload that is more explicit on the inner overload than it is + * on the external overload.) + * + * So in the end, we use this helper to remove the `this` annotation from the + * exported function types, while keeping it in the internal implementation. + * + */ +type RemoveThis = T extends (this: any) => infer R ? () => R : never; + +/** + * For internal purposes only - please call `ApolloClient.getMemoryInternals` instead + * @internal + */ +export const getApolloClientMemoryInternals = + __DEV__ ? + (_getApolloClientMemoryInternals as RemoveThis< + typeof _getApolloClientMemoryInternals + >) + : undefined; + +/** + * For internal purposes only - please call `ApolloClient.getMemoryInternals` instead + * @internal + */ +export const getInMemoryCacheMemoryInternals = + __DEV__ ? + (_getInMemoryCacheMemoryInternals as RemoveThis< + typeof _getInMemoryCacheMemoryInternals + >) + : undefined; + +/** + * For internal purposes only - please call `ApolloClient.getMemoryInternals` instead + * @internal + */ +export const getApolloCacheMemoryInternals = + __DEV__ ? + (_getApolloCacheMemoryInternals as RemoveThis< + typeof _getApolloCacheMemoryInternals + >) + : undefined; + +function getCurrentCacheSizes() { + // `defaultCacheSizes` is a `const enum` that will be inlined during build, so we have to reconstruct it's shape here + const defaults: Record = { + parser: defaultCacheSizes["parser"], + canonicalStringify: defaultCacheSizes["canonicalStringify"], + print: defaultCacheSizes["print"], + "documentTransform.cache": defaultCacheSizes["documentTransform.cache"], + "queryManager.getDocumentInfo": + defaultCacheSizes["queryManager.getDocumentInfo"], + "PersistedQueryLink.persistedQueryHashes": + defaultCacheSizes["PersistedQueryLink.persistedQueryHashes"], + "fragmentRegistry.transform": + defaultCacheSizes["fragmentRegistry.transform"], + "fragmentRegistry.lookup": defaultCacheSizes["fragmentRegistry.lookup"], + "fragmentRegistry.findFragmentSpreads": + defaultCacheSizes["fragmentRegistry.findFragmentSpreads"], + "cache.fragmentQueryDocuments": + defaultCacheSizes["cache.fragmentQueryDocuments"], + "removeTypenameFromVariables.getVariableDefinitions": + defaultCacheSizes["removeTypenameFromVariables.getVariableDefinitions"], + "inMemoryCache.maybeBroadcastWatch": + defaultCacheSizes["inMemoryCache.maybeBroadcastWatch"], + "inMemoryCache.executeSelectionSet": + defaultCacheSizes["inMemoryCache.executeSelectionSet"], + "inMemoryCache.executeSubSelectedArray": + defaultCacheSizes["inMemoryCache.executeSubSelectedArray"], + }; + return Object.fromEntries( + Object.entries(defaults).map(([k, v]) => [ + k, + cacheSizes[k as keyof CacheSizes] || v, + ]) + ); +} + +function _getApolloClientMemoryInternals(this: ApolloClient) { + if (!__DEV__) throw new Error("only supported in development mode"); + + return { + limits: getCurrentCacheSizes(), + sizes: { + print: globalCaches.print?.(), + parser: globalCaches.parser?.(), + canonicalStringify: globalCaches.canonicalStringify?.(), + links: linkInfo(this.link), + queryManager: { + getDocumentInfo: this["queryManager"]["transformCache"].size, + documentTransforms: transformInfo( + this["queryManager"].documentTransform + ), + }, + ...(this.cache.getMemoryInternals?.() as Partial< + ReturnType + > & + Partial>), + }, + }; +} + +function _getApolloCacheMemoryInternals(this: ApolloCache) { + return { + cache: { + fragmentQueryDocuments: getWrapperInformation(this["getFragmentDoc"]), + }, + }; +} + +function _getInMemoryCacheMemoryInternals(this: InMemoryCache) { + const fragments = this.config.fragments as + | undefined + | { + findFragmentSpreads?: Function; + transform?: Function; + lookup?: Function; + }; + + return { + ..._getApolloCacheMemoryInternals.apply(this as any), + addTypenameDocumentTransform: transformInfo(this["addTypenameTransform"]), + inMemoryCache: { + executeSelectionSet: getWrapperInformation( + this["storeReader"]["executeSelectionSet"] + ), + executeSubSelectedArray: getWrapperInformation( + this["storeReader"]["executeSubSelectedArray"] + ), + maybeBroadcastWatch: getWrapperInformation(this["maybeBroadcastWatch"]), + }, + fragmentRegistry: { + findFragmentSpreads: getWrapperInformation( + fragments?.findFragmentSpreads + ), + lookup: getWrapperInformation(fragments?.lookup), + transform: getWrapperInformation(fragments?.transform), + }, + }; +} + +function isWrapper(f?: Function): f is OptimisticWrapperFunction { + return !!f && "dirtyKey" in f; +} + +function getWrapperInformation(f?: Function) { + return isWrapper(f) ? f.size : undefined; +} + +function isDefined(value: T | undefined | null): value is T { + return value != null; +} + +function transformInfo(transform?: DocumentTransform) { + return recurseTransformInfo(transform).map((cache) => ({ cache })); +} + +function recurseTransformInfo(transform?: DocumentTransform): number[] { + return transform ? + [ + getWrapperInformation(transform?.["performWork"]), + ...recurseTransformInfo(transform?.["left"]), + ...recurseTransformInfo(transform?.["right"]), + ].filter(isDefined) + : []; +} + +function linkInfo(link?: ApolloLink): unknown[] { + return link ? + [ + link?.getMemoryInternals?.(), + ...linkInfo(link?.left), + ...linkInfo(link?.right), + ].filter(isDefined) + : []; +} diff --git a/src/utilities/caching/index.ts b/src/utilities/caching/index.ts new file mode 100644 index 00000000000..159dc27fcfd --- /dev/null +++ b/src/utilities/caching/index.ts @@ -0,0 +1,3 @@ +export { AutoCleanedStrongCache, AutoCleanedWeakCache } from "./caches.js"; +export type { CacheSizes } from "./sizes.js"; +export { cacheSizes, defaultCacheSizes } from "./sizes.js"; diff --git a/src/utilities/caching/sizes.ts b/src/utilities/caching/sizes.ts new file mode 100644 index 00000000000..114ce2118bc --- /dev/null +++ b/src/utilities/caching/sizes.ts @@ -0,0 +1,319 @@ +import { global } from "../globals/index.js"; + +declare global { + interface Window { + [cacheSizeSymbol]?: Partial; + } +} + +/** + * The cache sizes used by various Apollo Client caches. + * + * @remarks + * All configurable caches hold memoized values. If an item is + * cache-collected, it incurs only a small performance impact and + * doesn't cause data loss. A smaller cache size might save you memory. + * + * You should choose cache sizes appropriate for storing a reasonable + * number of values rather than every value. To prevent too much recalculation, + * choose cache sizes that are at least large enough to hold memoized values for + * all hooks/queries on the screen at any given time. + */ +/* + * We assume a "base value" of 1000 here, which is already very generous. + * In most applications, it will be very unlikely that 1000 different queries + * are on screen at the same time. + */ +export interface CacheSizes { + /** + * Cache size for the [`print`](https://github.com/apollographql/apollo-client/blob/main/src/utilities/graphql/print.ts) function. + * + * It is called with transformed `DocumentNode`s. + * + * @defaultValue + * Defaults to `2000`. + * + * @remarks + * This method is called to transform a GraphQL query AST parsed by `gql` + * back into a GraphQL string. + * + * @privateRemarks + * This method is called from the `QueryManager` and various `ApolloLink`s, + * always with the "serverQuery", so the server-facing part of a transformed + * `DocumentNode`. + */ + print: number; + /** + * Cache size for the [`parser`](https://github.com/apollographql/apollo-client/blob/main/src/react/parser/index.ts) function. + * + * It is called with user-provided `DocumentNode`s. + * + * @defaultValue + * Defaults to `1000`. + * + * @remarks + * This method is called by HOCs and hooks. + * + * @privateRemarks + * This function is used directly in HOCs, and nowadays mainly accessed by + * calling `verifyDocumentType` from various hooks. + * It is called with a user-provided DocumentNode. + */ + parser: number; + /** + * Cache size for the cache of [`DocumentTransform`](https://github.com/apollographql/apollo-client/blob/main/src/utilities/graphql/DocumentTransform.ts) + * instances with the `cache` option set to `true`. + * + * Can be called with user-defined or already-transformed `DocumentNode`s. + * + * @defaultValue + * Defaults to `2000`. + * + * @remarks + * The cache size here should be chosen with other `DocumentTransform`s in mind. + * For example, if there was a `DocumentTransform` that would take `x` `DocumentNode`s, + * and returned a differently-transformed `DocumentNode` depending if the app is + * online or offline, then we assume that the cache returns `2*x` documents. + * If that were concatenated with another `DocumentTransform` that would + * also duplicate the cache size, you'd need to account for `4*x` documents + * returned by the second transform. + * + * Due to an implementation detail of Apollo Client, if you use custom document + * transforms you should always add `n` (the "base" number of user-provided + * Documents) to the resulting cache size. + * + * If we assume that the user-provided transforms receive `n` documents and + * return `n` documents, the cache size should be `2*n`. + * + * If we assume that the chain of user-provided transforms receive `n` documents and + * return `4*n` documents, the cache size should be `5*n`. + * + * This size should also then be used in every other cache that mentions that + * it operates on a "transformed" `DocumentNode`. + * + * @privateRemarks + * Cache size for the `performWork` method of each [`DocumentTransform`](https://github.com/apollographql/apollo-client/blob/main/src/utilities/graphql/DocumentTransform.ts). + * + * No user-provided DocumentNode will actually be "the last one", as we run the + * `defaultDocumentTransform` before *and* after the user-provided transforms. + * For that reason, we need the extra `n` here - `n` for "before transformation" + * plus the actual maximum cache size of the user-provided transform chain. + * + * This method is called from `transformDocument`, which is called from + * `QueryManager` with a user-provided DocumentNode. + * It is also called with already-transformed DocumentNodes, assuming the + * user provided additional transforms. + * + */ + "documentTransform.cache": number; + /** + * A cache inside of [`QueryManager`](https://github.com/apollographql/apollo-client/blob/main/src/core/QueryManager.ts). + * + * It is called with transformed `DocumentNode`s. + * + * @defaultValue + * Defaults to `2000`. + * + * @privateRemarks + * Cache size for the `transformCache` used in the `getDocumentInfo` method of `QueryManager`. + * Called throughout the `QueryManager` with transformed DocumentNodes. + */ + "queryManager.getDocumentInfo": number; + /** + * A cache inside of [`PersistedQueryLink`](https://github.com/apollographql/apollo-client/blob/main/src/link/persisted-queries/index.ts). + * + * It is called with transformed `DocumentNode`s. + * + * @defaultValue + * Defaults to `2000`. + * + * @remarks + * This cache is used to cache the hashes of persisted queries. + * + * @privateRemarks + * Cache size for the `hashesByQuery` cache in the `PersistedQueryLink`. + */ + "PersistedQueryLink.persistedQueryHashes": number; + /** + * Cache used by [`canonicalStringify`](https://github.com/apollographql/apollo-client/blob/main/src/utilities/common/canonicalStringify.ts). + * + * @defaultValue + * Defaults to `1000`. + * + * @remarks + * This cache contains the sorted keys of objects that are stringified by + * `canonicalStringify`. + * It uses the stringified unsorted keys of objects as keys. + * The cache will not grow beyond the size of different object **shapes** + * encountered in an application, no matter how much actual data gets stringified. + * + * @privateRemarks + * Cache size for the `sortingMap` in `canonicalStringify`. + */ + canonicalStringify: number; + /** + * A cache inside of [`FragmentRegistry`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/fragmentRegistry.ts). + * + * Can be called with user-defined or already-transformed `DocumentNode`s. + * + * @defaultValue + * Defaults to `2000`. + * + * @privateRemarks + * + * Cache size for the `transform` method of FragmentRegistry. + * This function is called as part of the `defaultDocumentTransform` which will be called with + * user-provided and already-transformed DocumentNodes. + * + */ + "fragmentRegistry.transform": number; + /** + * A cache inside of [`FragmentRegistry`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/fragmentRegistry.ts). + * + * This function is called with fragment names in the form of a string. + * + * @defaultValue + * Defaults to `1000`. + * + * @remarks + * The size of this case should be chosen with the number of fragments in + * your application in mind. + * + * Note: + * This function is a dependency of `fragmentRegistry.transform`, so having too small of a cache size here + * might involuntarily invalidate values in the `transform` cache. + * + * @privateRemarks + * Cache size for the `lookup` method of FragmentRegistry. + */ + "fragmentRegistry.lookup": number; + /** + * Cache size for the `findFragmentSpreads` method of [`FragmentRegistry`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/fragmentRegistry.ts). + * + * This function is called with transformed `DocumentNode`s, as well as recursively + * with every fragment spread referenced within that, or a fragment referenced by a + * fragment spread. + * + * @defaultValue + * Defaults to `4000`. + * + * @remarks + * + * Note: This function is a dependency of `fragmentRegistry.transform`, so having too small of cache size here + * might involuntarily invalidate values in the `transform` cache. + */ + "fragmentRegistry.findFragmentSpreads": number; + /** + * Cache size for the `getFragmentDoc` method of [`ApolloCache`](https://github.com/apollographql/apollo-client/blob/main/src/cache/core/cache.ts). + * + * This function is called with user-provided fragment definitions. + * + * @defaultValue + * Defaults to `1000`. + * + * @remarks + * This function is called from `readFragment` with user-provided fragment definitions. + */ + "cache.fragmentQueryDocuments": number; + /** + * Cache used in [`removeTypenameFromVariables`](https://github.com/apollographql/apollo-client/blob/main/src/link/remove-typename/removeTypenameFromVariables.ts). + * + * This function is called transformed `DocumentNode`s. + * + * @defaultValue + * Defaults to `2000`. + * + * @privateRemarks + * Cache size for the `getVariableDefinitions` function of `removeTypenameFromVariables`. + */ + "removeTypenameFromVariables.getVariableDefinitions": number; + /** + * Cache size for the `maybeBroadcastWatch` method on [`InMemoryCache`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/inMemoryCache.ts). + * + * Note: `maybeBroadcastWatch` will be set to the `resultCacheMaxSize` option and + * will fall back to this configuration value if the option is not set. + * + * @defaultValue + * Defaults to `5000`. + * + * @remarks + * This method is used for dependency tracking in the `InMemoryCache` and + * prevents from unnecessary re-renders. + * It is recommended to keep this value significantly higher than the number of + * possible subscribers you will have active at the same time in your application + * at any time. + */ + "inMemoryCache.maybeBroadcastWatch": number; + /** + * Cache size for the `executeSelectionSet` method on [`StoreReader`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/readFromStore.ts). + * + * Note: + * `executeSelectionSet` will be set to the `resultCacheMaxSize` option and + * will fall back to this configuration value if the option is not set. + * + * @defaultValue + * Defaults to `10000`. + * + * @remarks + * Every object that is read from the cache will be cached here, so it is + * recommended to set this to a high value. + */ + "inMemoryCache.executeSelectionSet": number; + /** + * Cache size for the `executeSubSelectedArray` method on [`StoreReader`](https://github.com/apollographql/apollo-client/blob/main/src/cache/inmemory/readFromStore.ts). + * + * Note: + * `executeSubSelectedArray` will be set to the `resultCacheMaxSize` option and + * will fall back to this configuration value if the option is not set. + * + * @defaultValue + * Defaults to `5000`. + * + * @remarks + * Every array that is read from the cache will be cached here, so it is + * recommended to set this to a high value. + */ + "inMemoryCache.executeSubSelectedArray": number; +} + +const cacheSizeSymbol = Symbol.for("apollo.cacheSize"); +/** + * + * The global cache size configuration for Apollo Client. + * + * @remarks + * + * You can directly modify this object, but any modification will + * only have an effect on caches that are created after the modification. + * + * So for global caches, such as `parser`, `canonicalStringify` and `print`, + * you might need to call `.reset` on them, which will essentially re-create them. + * + * Alternatively, you can set `globalThis[Symbol.for("apollo.cacheSize")]` before + * you load the Apollo Client package: + * + * @example + * ```ts + * globalThis[Symbol.for("apollo.cacheSize")] = { + * parser: 100 + * } satisfies Partial // the `satisfies` is optional if using TypeScript + * ``` + */ +export const cacheSizes: Partial = { ...global[cacheSizeSymbol] }; + +export const enum defaultCacheSizes { + parser = 1000, + canonicalStringify = 1000, + print = 2000, + "documentTransform.cache" = 2000, + "queryManager.getDocumentInfo" = 2000, + "PersistedQueryLink.persistedQueryHashes" = 2000, + "fragmentRegistry.transform" = 2000, + "fragmentRegistry.lookup" = 1000, + "fragmentRegistry.findFragmentSpreads" = 4000, + "cache.fragmentQueryDocuments" = 1000, + "removeTypenameFromVariables.getVariableDefinitions" = 2000, + "inMemoryCache.maybeBroadcastWatch" = 5000, + "inMemoryCache.executeSelectionSet" = 50000, + "inMemoryCache.executeSubSelectedArray" = 10000, +} diff --git a/src/utilities/common/__tests__/canonicalStringify.ts b/src/utilities/common/__tests__/canonicalStringify.ts new file mode 100644 index 00000000000..a82f18e3863 --- /dev/null +++ b/src/utilities/common/__tests__/canonicalStringify.ts @@ -0,0 +1,110 @@ +import { canonicalStringify } from "../canonicalStringify"; + +function forEachPermutation( + keys: string[], + callback: (permutation: string[]) => void +) { + if (keys.length <= 1) { + callback(keys); + return; + } + const first = keys[0]; + const rest = keys.slice(1); + forEachPermutation(rest, (permutation) => { + for (let i = 0; i <= permutation.length; ++i) { + callback([...permutation.slice(0, i), first, ...permutation.slice(i)]); + } + }); +} + +function allObjectPermutations>(obj: T) { + const keys = Object.keys(obj); + const permutations: T[] = []; + forEachPermutation(keys, (permutation) => { + const permutationObj = Object.create(Object.getPrototypeOf(obj)); + permutation.forEach((key) => { + permutationObj[key] = obj[key]; + }); + permutations.push(permutationObj); + }); + return permutations; +} + +describe("canonicalStringify", () => { + beforeEach(() => { + canonicalStringify.reset(); + }); + + it("should not modify original object", () => { + const obj = { c: 3, a: 1, b: 2 }; + expect(canonicalStringify(obj)).toBe('{"a":1,"b":2,"c":3}'); + expect(Object.keys(obj)).toEqual(["c", "a", "b"]); + }); + + it("forEachPermutation should work", () => { + const permutations: string[][] = []; + forEachPermutation(["a", "b", "c"], (permutation) => { + permutations.push(permutation); + }); + expect(permutations).toEqual([ + ["a", "b", "c"], + ["b", "a", "c"], + ["b", "c", "a"], + ["a", "c", "b"], + ["c", "a", "b"], + ["c", "b", "a"], + ]); + }); + + it("canonicalStringify should stably stringify all permutations of an object", () => { + const unstableStrings = new Set(); + const stableStrings = new Set(); + + allObjectPermutations({ + c: 3, + a: 1, + b: 2, + }).forEach((obj) => { + unstableStrings.add(JSON.stringify(obj)); + stableStrings.add(canonicalStringify(obj)); + + expect(canonicalStringify(obj)).toBe('{"a":1,"b":2,"c":3}'); + + allObjectPermutations({ + z: "z", + y: ["y", obj, "why"], + x: "x", + }).forEach((parent) => { + expect(canonicalStringify(parent)).toBe( + '{"x":"x","y":["y",{"a":1,"b":2,"c":3},"why"],"z":"z"}' + ); + }); + }); + + expect(unstableStrings.size).toBe(6); + expect(stableStrings.size).toBe(1); + }); + + it("should not modify keys of custom-prototype objects", () => { + class Custom { + z = "z"; + y = "y"; + x = "x"; + b = "b"; + a = "a"; + c = "c"; + } + + const obj = { + z: "z", + x: "x", + y: new Custom(), + }; + + expect(Object.keys(obj.y)).toEqual(["z", "y", "x", "b", "a", "c"]); + + expect(canonicalStringify(obj)).toBe( + '{"x":"x","y":{"z":"z","y":"y","x":"x","b":"b","a":"a","c":"c"},"z":"z"}' + ); + }); +}); diff --git a/src/utilities/common/canUse.ts b/src/utilities/common/canUse.ts index 72e56c70388..217cc01158b 100644 --- a/src/utilities/common/canUse.ts +++ b/src/utilities/common/canUse.ts @@ -2,7 +2,9 @@ import { maybe } from "../globals/index.js"; export const canUseWeakMap = typeof WeakMap === "function" && - maybe(() => navigator.product) !== "ReactNative"; + !maybe( + () => navigator.product == "ReactNative" && !(global as any).HermesInternal + ); export const canUseWeakSet = typeof WeakSet === "function"; diff --git a/src/utilities/common/canonicalStringify.ts b/src/utilities/common/canonicalStringify.ts new file mode 100644 index 00000000000..adcc9898211 --- /dev/null +++ b/src/utilities/common/canonicalStringify.ts @@ -0,0 +1,100 @@ +import { + AutoCleanedStrongCache, + cacheSizes, + defaultCacheSizes, +} from "../../utilities/caching/index.js"; +import { registerGlobalCache } from "../caching/getMemoryInternals.js"; + +/** + * Like JSON.stringify, but with object keys always sorted in the same order. + * + * To achieve performant sorting, this function uses a Map from JSON-serialized + * arrays of keys (in any order) to sorted arrays of the same keys, with a + * single sorted array reference shared by all permutations of the keys. + * + * As a drawback, this function will add a little bit more memory for every + * object encountered that has different (more, less, a different order of) keys + * than in the past. + * + * In a typical application, this extra memory usage should not play a + * significant role, as `canonicalStringify` will be called for only a limited + * number of object shapes, and the cache will not grow beyond a certain point. + * But in some edge cases, this could be a problem, so we provide + * canonicalStringify.reset() as a way of clearing the cache. + * */ +export const canonicalStringify = Object.assign( + function canonicalStringify(value: any): string { + return JSON.stringify(value, stableObjectReplacer); + }, + { + reset() { + // Clearing the sortingMap will reclaim all cached memory, without + // affecting the logical results of canonicalStringify, but potentially + // sacrificing performance until the cache is refilled. + sortingMap = new AutoCleanedStrongCache( + cacheSizes.canonicalStringify || defaultCacheSizes.canonicalStringify + ); + }, + } +); + +if (__DEV__) { + registerGlobalCache("canonicalStringify", () => sortingMap.size); +} + +// Values are JSON-serialized arrays of object keys (in any order), and values +// are sorted arrays of the same keys. +let sortingMap!: AutoCleanedStrongCache; +canonicalStringify.reset(); + +// The JSON.stringify function takes an optional second argument called a +// replacer function. This function is called for each key-value pair in the +// object being stringified, and its return value is used instead of the +// original value. If the replacer function returns a new value, that value is +// stringified as JSON instead of the original value of the property. +// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#the_replacer_parameter +function stableObjectReplacer(key: string, value: any) { + if (value && typeof value === "object") { + const proto = Object.getPrototypeOf(value); + // We don't want to mess with objects that are not "plain" objects, which + // means their prototype is either Object.prototype or null. This check also + // prevents needlessly rearranging the indices of arrays. + if (proto === Object.prototype || proto === null) { + const keys = Object.keys(value); + // If keys is already sorted, let JSON.stringify serialize the original + // value instead of creating a new object with keys in the same order. + if (keys.every(everyKeyInOrder)) return value; + const unsortedKey = JSON.stringify(keys); + let sortedKeys = sortingMap.get(unsortedKey); + if (!sortedKeys) { + keys.sort(); + const sortedKey = JSON.stringify(keys); + // Checking for sortedKey in the sortingMap allows us to share the same + // sorted array reference for all permutations of the same set of keys. + sortedKeys = sortingMap.get(sortedKey) || keys; + sortingMap.set(unsortedKey, sortedKeys); + sortingMap.set(sortedKey, sortedKeys); + } + const sortedObject = Object.create(proto); + // Reassigning the keys in sorted order will cause JSON.stringify to + // serialize them in sorted order. + sortedKeys.forEach((key) => { + sortedObject[key] = value[key]; + }); + return sortedObject; + } + } + return value; +} + +// Since everything that happens in stableObjectReplacer benefits from being as +// efficient as possible, we use a static function as the callback for +// keys.every in order to test if the provided keys are already sorted without +// allocating extra memory for a callback. +function everyKeyInOrder( + key: string, + i: number, + keys: readonly string[] +): boolean { + return i === 0 || keys[i - 1] <= key; +} diff --git a/src/utilities/graphql/DocumentTransform.ts b/src/utilities/graphql/DocumentTransform.ts index 9ec1cd3a63b..ec6e0e44152 100644 --- a/src/utilities/graphql/DocumentTransform.ts +++ b/src/utilities/graphql/DocumentTransform.ts @@ -3,13 +3,28 @@ import { canUseWeakMap, canUseWeakSet } from "../common/canUse.js"; import { checkDocument } from "./getFromAST.js"; import { invariant } from "../globals/index.js"; import type { DocumentNode } from "graphql"; +import { WeakCache } from "@wry/caches"; +import { wrap } from "optimism"; +import { cacheSizes } from "../caching/index.js"; export type DocumentTransformCacheKey = ReadonlyArray; type TransformFn = (document: DocumentNode) => DocumentNode; interface DocumentTransformOptions { + /** + * Determines whether to cache the transformed GraphQL document. Caching can speed up repeated calls to the document transform for the same input document. Set to `false` to completely disable caching for the document transform. When disabled, this option takes precedence over the [`getCacheKey`](#getcachekey) option. + * + * The default value is `true`. + */ cache?: boolean; + /** + * Defines a custom cache key for a GraphQL document that will determine whether to re-run the document transform when given the same input GraphQL document. Returns an array that defines the cache key. Return `undefined` to disable caching for that GraphQL document. + * + * > **Note:** The items in the array may be any type, but also need to be referentially stable to guarantee a stable cache key. + * + * The default implementation of this function returns the `document` as the cache key. + */ getCacheKey?: ( document: DocumentNode ) => DocumentTransformCacheKey | undefined; @@ -21,14 +36,11 @@ function identity(document: DocumentNode) { export class DocumentTransform { private readonly transform: TransformFn; + private cached: boolean; private readonly resultCache = canUseWeakSet ? new WeakSet() : new Set(); - private stableCacheKeys: - | Trie<{ key: DocumentTransformCacheKey; value?: DocumentNode }> - | undefined; - // This default implementation of getCacheKey can be overridden by providing // options.getCacheKey to the DocumentTransform constructor. In general, a // getCacheKey function may either return an array of keys (often including @@ -52,14 +64,17 @@ export class DocumentTransform { left: DocumentTransform, right: DocumentTransform = DocumentTransform.identity() ) { - return new DocumentTransform( - (document) => { - const documentTransform = predicate(document) ? left : right; - - return documentTransform.transformDocument(document); - }, - // Reasonably assume both `left` and `right` transforms handle their own caching - { cache: false } + return Object.assign( + new DocumentTransform( + (document) => { + const documentTransform = predicate(document) ? left : right; + + return documentTransform.transformDocument(document); + }, + // Reasonably assume both `left` and `right` transforms handle their own caching + { cache: false } + ), + { left, right } ); } @@ -73,12 +88,42 @@ export class DocumentTransform { // Override default `getCacheKey` function, which returns [document]. this.getCacheKey = options.getCacheKey; } + this.cached = options.cache !== false; + + this.resetCache(); + } - if (options.cache !== false) { - this.stableCacheKeys = new Trie(canUseWeakMap, (key) => ({ key })); + /** + * Resets the internal cache of this transform, if it has one. + */ + resetCache() { + if (this.cached) { + const stableCacheKeys = new Trie(canUseWeakMap); + this.performWork = wrap( + DocumentTransform.prototype.performWork.bind(this), + { + makeCacheKey: (document) => { + const cacheKeys = this.getCacheKey(document); + if (cacheKeys) { + invariant( + Array.isArray(cacheKeys), + "`getCacheKey` must return an array or undefined" + ); + return stableCacheKeys.lookupArray(cacheKeys); + } + }, + max: cacheSizes["documentTransform.cache"], + cache: WeakCache, + } + ); } } + private performWork(document: DocumentNode) { + checkDocument(document); + return this.transform(document); + } + transformDocument(document: DocumentNode) { // If a user passes an already transformed result back to this function, // immediately return it. @@ -86,46 +131,39 @@ export class DocumentTransform { return document; } - const cacheEntry = this.getStableCacheEntry(document); - - if (cacheEntry && cacheEntry.value) { - return cacheEntry.value; - } - - checkDocument(document); - - const transformedDocument = this.transform(document); + const transformedDocument = this.performWork(document); this.resultCache.add(transformedDocument); - if (cacheEntry) { - cacheEntry.value = transformedDocument; - } - return transformedDocument; } - concat(otherTransform: DocumentTransform) { - return new DocumentTransform( - (document) => { - return otherTransform.transformDocument( - this.transformDocument(document) - ); - }, - // Reasonably assume both transforms handle their own caching - { cache: false } + concat(otherTransform: DocumentTransform): DocumentTransform { + return Object.assign( + new DocumentTransform( + (document) => { + return otherTransform.transformDocument( + this.transformDocument(document) + ); + }, + // Reasonably assume both transforms handle their own caching + { cache: false } + ), + { + left: this, + right: otherTransform, + } ); } - getStableCacheEntry(document: DocumentNode) { - if (!this.stableCacheKeys) return; - const cacheKeys = this.getCacheKey(document); - if (cacheKeys) { - invariant( - Array.isArray(cacheKeys), - "`getCacheKey` must return an array or undefined" - ); - return this.stableCacheKeys.lookupArray(cacheKeys); - } - } + /** + * @internal + * Used to iterate through all transforms that are concatenations or `split` links. + */ + readonly left?: DocumentTransform; + /** + * @internal + * Used to iterate through all transforms that are concatenations or `split` links. + */ + readonly right?: DocumentTransform; } diff --git a/src/utilities/graphql/print.ts b/src/utilities/graphql/print.ts index 5fb1cb68599..20e779a9a55 100644 --- a/src/utilities/graphql/print.ts +++ b/src/utilities/graphql/print.ts @@ -1,14 +1,33 @@ +import type { ASTNode } from "graphql"; import { print as origPrint } from "graphql"; -import { canUseWeakMap } from "../common/canUse.js"; +import { + AutoCleanedWeakCache, + cacheSizes, + defaultCacheSizes, +} from "../caching/index.js"; +import { registerGlobalCache } from "../caching/getMemoryInternals.js"; -const printCache = canUseWeakMap ? new WeakMap() : undefined; -export const print: typeof origPrint = (ast) => { - let result; - result = printCache?.get(ast); +let printCache!: AutoCleanedWeakCache; +export const print = Object.assign( + (ast: ASTNode) => { + let result = printCache.get(ast); - if (!result) { - result = origPrint(ast); - printCache?.set(ast, result); + if (!result) { + result = origPrint(ast); + printCache.set(ast, result); + } + return result; + }, + { + reset() { + printCache = new AutoCleanedWeakCache( + cacheSizes.print || defaultCacheSizes.print + ); + }, } - return result; -}; +); +print.reset(); + +if (__DEV__) { + registerGlobalCache("print", () => (printCache ? printCache.size : 0)); +} diff --git a/src/utilities/graphql/storeUtils.ts b/src/utilities/graphql/storeUtils.ts index b6a4cab68c4..c96e0628eb8 100644 --- a/src/utilities/graphql/storeUtils.ts +++ b/src/utilities/graphql/storeUtils.ts @@ -24,6 +24,7 @@ import type { import { isNonNullObject } from "../common/objects.js"; import type { FragmentMap } from "./fragments.js"; import { getFragmentFromSelection } from "./fragments.js"; +import { canonicalStringify } from "../common/canonicalStringify.js"; export interface Reference { readonly __ref: string; @@ -212,6 +213,11 @@ const KNOWN_DIRECTIVES: string[] = [ "nonreactive", ]; +// Default stable JSON.stringify implementation used by getStoreKeyName. Can be +// updated/replaced with something better by calling +// getStoreKeyName.setStringify(newStringifyFunction). +let storeKeyNameStringify: (value: any) => string = canonicalStringify; + export const getStoreKeyName = Object.assign( function ( fieldName: string, @@ -239,7 +245,9 @@ export const getStoreKeyName = Object.assign( filteredArgs[key] = args[key]; }); - return `${directives["connection"]["key"]}(${stringify(filteredArgs)})`; + return `${directives["connection"]["key"]}(${storeKeyNameStringify( + filteredArgs + )})`; } else { return directives["connection"]["key"]; } @@ -251,7 +259,7 @@ export const getStoreKeyName = Object.assign( // We can't use `JSON.stringify` here since it's non-deterministic, // and can lead to different store key names being created even though // the `args` object used during creation has the same properties/values. - const stringifiedArgs: string = stringify(args); + const stringifiedArgs: string = storeKeyNameStringify(args); completeFieldName += `(${stringifiedArgs})`; } @@ -259,7 +267,9 @@ export const getStoreKeyName = Object.assign( Object.keys(directives).forEach((key) => { if (KNOWN_DIRECTIVES.indexOf(key) !== -1) return; if (directives[key] && Object.keys(directives[key]).length) { - completeFieldName += `@${key}(${stringify(directives[key])})`; + completeFieldName += `@${key}(${storeKeyNameStringify( + directives[key] + )})`; } else { completeFieldName += `@${key}`; } @@ -269,35 +279,14 @@ export const getStoreKeyName = Object.assign( return completeFieldName; }, { - setStringify(s: typeof stringify) { - const previous = stringify; - stringify = s; + setStringify(s: typeof storeKeyNameStringify) { + const previous = storeKeyNameStringify; + storeKeyNameStringify = s; return previous; }, } ); -// Default stable JSON.stringify implementation. Can be updated/replaced with -// something better by calling getStoreKeyName.setStringify. -let stringify = function defaultStringify(value: any): string { - return JSON.stringify(value, stringifyReplacer); -}; - -function stringifyReplacer(_key: string, value: any): any { - if (isNonNullObject(value) && !Array.isArray(value)) { - value = Object.keys(value) - .sort() - .reduce( - (copy, key) => { - copy[key] = value[key]; - return copy; - }, - {} as Record - ); - } - return value; -} - export function argumentsObjectFromField( field: FieldNode | DirectiveNode, variables?: Record diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 5f120fd4290..637ae100af7 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -98,6 +98,7 @@ export type { } from "./observables/Observable.js"; export { Observable } from "./observables/Observable.js"; +export type { PromiseWithState } from "./promises/decoration.js"; export { isStatefulPromise, createFulfilledPromise, @@ -122,9 +123,19 @@ export * from "./common/stringifyForDisplay.js"; export * from "./common/mergeOptions.js"; export * from "./common/incrementalResult.js"; +export { canonicalStringify } from "./common/canonicalStringify.js"; export { omitDeep } from "./common/omitDeep.js"; export { stripTypename } from "./common/stripTypename.js"; export * from "./types/IsStrictlyAny.js"; export type { DeepOmit } from "./types/DeepOmit.js"; export type { DeepPartial } from "./types/DeepPartial.js"; +export type { OnlyRequiredProperties } from "./types/OnlyRequiredProperties.js"; + +export { + AutoCleanedStrongCache, + AutoCleanedWeakCache, + cacheSizes, + defaultCacheSizes, +} from "./caching/index.js"; +export type { CacheSizes } from "./caching/index.js"; diff --git a/src/utilities/observables/Concast.ts b/src/utilities/observables/Concast.ts index c2fbb6fb180..73c36520f8b 100644 --- a/src/utilities/observables/Concast.ts +++ b/src/utilities/observables/Concast.ts @@ -210,7 +210,10 @@ export class Concast extends Observable { // followed by a 'complete' message (see addObserver). iterateObserversSafely(this.observers, "complete"); } else if (isPromiseLike(value)) { - value.then((obs) => (this.sub = obs.subscribe(this.handlers))); + value.then( + (obs) => (this.sub = obs.subscribe(this.handlers)), + this.handlers.error + ); } else { this.sub = value.subscribe(this.handlers); } diff --git a/src/utilities/observables/__tests__/Concast.ts b/src/utilities/observables/__tests__/Concast.ts index d6ce248159f..b590cde2fb8 100644 --- a/src/utilities/observables/__tests__/Concast.ts +++ b/src/utilities/observables/__tests__/Concast.ts @@ -1,5 +1,5 @@ import { itAsync } from "../../../testing/core"; -import { Observable } from "../Observable"; +import { Observable, Observer } from "../Observable"; import { Concast, ConcastSourcesIterable } from "../Concast"; describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { @@ -187,4 +187,115 @@ describe("Concast Observable (similar to Behavior Subject in RxJS)", () => { sub.unsubscribe(); }); }); + + it("resolving all sources of a concast frees all observer references on `this.observers`", async () => { + const { promise, resolve } = deferred>(); + const observers: Observer[] = [{ next() {} }]; + const observerRefs = observers.map((observer) => new WeakRef(observer)); + + const concast = new Concast([Observable.of(1, 2), promise]); + + concast.subscribe(observers[0]); + delete observers[0]; + + expect(concast["observers"].size).toBe(1); + + resolve(Observable.of(3, 4)); + + await expect(concast.promise).resolves.toBe(4); + + await expect(observerRefs[0]).toBeGarbageCollected(); + }); + + it("rejecting a source-wrapping promise of a concast frees all observer references on `this.observers`", async () => { + const { promise, reject } = deferred>(); + let subscribingObserver: Observer | undefined = { + next() {}, + error() {}, + }; + const subscribingObserverRef = new WeakRef(subscribingObserver); + + const concast = new Concast([ + Observable.of(1, 2), + promise, + // just to ensure this also works if the cancelling source is not the last source + Observable.of(3, 5), + ]); + + concast.subscribe(subscribingObserver); + + expect(concast["observers"].size).toBe(1); + + reject("error"); + await expect(concast.promise).rejects.toBe("error"); + subscribingObserver = undefined; + await expect(subscribingObserverRef).toBeGarbageCollected(); + }); + + it("rejecting a source of a concast frees all observer references on `this.observers`", async () => { + let subscribingObserver: Observer | undefined = { + next() {}, + error() {}, + }; + const subscribingObserverRef = new WeakRef(subscribingObserver); + + let sourceObserver!: Observer; + const sourceObservable = new Observable((o) => { + sourceObserver = o; + }); + + const concast = new Concast([ + Observable.of(1, 2), + sourceObservable, + Observable.of(3, 5), + ]); + + concast.subscribe(subscribingObserver); + + expect(concast["observers"].size).toBe(1); + + await Promise.resolve(); + sourceObserver.error!("error"); + await expect(concast.promise).rejects.toBe("error"); + subscribingObserver = undefined; + await expect(subscribingObserverRef).toBeGarbageCollected(); + }); + + it("after subscribing to an already-resolved concast, the reference is freed up again", async () => { + const concast = new Concast([Observable.of(1, 2)]); + await expect(concast.promise).resolves.toBe(2); + await Promise.resolve(); + + let sourceObserver: Observer | undefined = { next() {}, error() {} }; + const sourceObserverRef = new WeakRef(sourceObserver); + + concast.subscribe(sourceObserver); + + sourceObserver = undefined; + await expect(sourceObserverRef).toBeGarbageCollected(); + }); + + it("after subscribing to an already-rejected concast, the reference is freed up again", async () => { + const concast = new Concast([Promise.reject("error")]); + await expect(concast.promise).rejects.toBe("error"); + await Promise.resolve(); + + let sourceObserver: Observer | undefined = { next() {}, error() {} }; + const sourceObserverRef = new WeakRef(sourceObserver); + + concast.subscribe(sourceObserver); + + sourceObserver = undefined; + await expect(sourceObserverRef).toBeGarbageCollected(); + }); }); + +function deferred() { + let resolve!: (v: X) => void; + let reject!: (e: any) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { resolve, reject, promise }; +} diff --git a/src/utilities/subscriptions/relay/index.ts b/src/utilities/subscriptions/relay/index.ts new file mode 100644 index 00000000000..1c92c074fea --- /dev/null +++ b/src/utilities/subscriptions/relay/index.ts @@ -0,0 +1,60 @@ +import { Observable } from "relay-runtime"; +import type { RequestParameters, GraphQLResponse } from "relay-runtime"; +import { + handleError, + readMultipartBody, +} from "../../../link/http/parseAndCheckHttpResponse.js"; +import { maybe } from "../../index.js"; +import { serializeFetchParameter } from "../../../core/index.js"; + +import type { OperationVariables } from "../../../core/index.js"; +import type { Body } from "../../../link/http/selectHttpOptionsAndBody.js"; +import { generateOptionsForMultipartSubscription } from "../shared.js"; +import type { CreateMultipartSubscriptionOptions } from "../shared.js"; + +const backupFetch = maybe(() => fetch); + +export function createFetchMultipartSubscription( + uri: string, + { fetch: preferredFetch, headers }: CreateMultipartSubscriptionOptions = {} +) { + return function fetchMultipartSubscription( + operation: RequestParameters, + variables: OperationVariables + ): Observable { + const body: Body = { + operationName: operation.name, + variables, + query: operation.text || "", + }; + const options = generateOptionsForMultipartSubscription(headers || {}); + + return Observable.create((sink) => { + try { + options.body = serializeFetchParameter(body, "Payload"); + } catch (parseError) { + sink.error(parseError as Error); + } + + const currentFetch = preferredFetch || maybe(() => fetch) || backupFetch; + const observerNext = sink.next.bind(sink); + + currentFetch!(uri, options) + .then((response) => { + const ctype = response.headers?.get("content-type"); + + if (ctype !== null && /^multipart\/mixed/i.test(ctype)) { + return readMultipartBody(response, observerNext); + } + + sink.error(new Error("Expected multipart response")); + }) + .then(() => { + sink.complete(); + }) + .catch((err: any) => { + handleError(err, sink); + }); + }); + }; +} diff --git a/src/utilities/subscriptions/shared.ts b/src/utilities/subscriptions/shared.ts new file mode 100644 index 00000000000..f3706dab11e --- /dev/null +++ b/src/utilities/subscriptions/shared.ts @@ -0,0 +1,21 @@ +import { fallbackHttpConfig } from "../../link/http/selectHttpOptionsAndBody.js"; + +export type CreateMultipartSubscriptionOptions = { + fetch?: WindowOrWorkerGlobalScope["fetch"]; + headers?: Record; +}; + +export function generateOptionsForMultipartSubscription( + headers: Record +) { + const options: { headers: Record; body?: string } = { + ...fallbackHttpConfig.options, + headers: { + ...(headers || {}), + ...fallbackHttpConfig.headers, + accept: + "multipart/mixed;boundary=graphql;subscriptionSpec=1.0,application/json", + }, + }; + return options; +} diff --git a/src/utilities/subscriptions/urql/index.ts b/src/utilities/subscriptions/urql/index.ts new file mode 100644 index 00000000000..26bf4d4fb57 --- /dev/null +++ b/src/utilities/subscriptions/urql/index.ts @@ -0,0 +1,56 @@ +import { Observable } from "../../index.js"; +import { + handleError, + readMultipartBody, +} from "../../../link/http/parseAndCheckHttpResponse.js"; +import { maybe } from "../../index.js"; +import { serializeFetchParameter } from "../../../core/index.js"; +import type { Body } from "../../../link/http/selectHttpOptionsAndBody.js"; +import { generateOptionsForMultipartSubscription } from "../shared.js"; +import type { CreateMultipartSubscriptionOptions } from "../shared.js"; + +const backupFetch = maybe(() => fetch); + +export function createFetchMultipartSubscription( + uri: string, + { fetch: preferredFetch, headers }: CreateMultipartSubscriptionOptions = {} +) { + return function multipartSubscriptionForwarder({ + query, + variables, + }: { + query?: string; + variables: undefined | Record; + }) { + const body: Body = { variables, query }; + const options = generateOptionsForMultipartSubscription(headers || {}); + + return new Observable((observer) => { + try { + options.body = serializeFetchParameter(body, "Payload"); + } catch (parseError) { + observer.error(parseError); + } + + const currentFetch = preferredFetch || maybe(() => fetch) || backupFetch; + const observerNext = observer.next.bind(observer); + + currentFetch!(uri, options) + .then((response) => { + const ctype = response.headers?.get("content-type"); + + if (ctype !== null && /^multipart\/mixed/i.test(ctype)) { + return readMultipartBody(response, observerNext); + } + + observer.error(new Error("Expected multipart response")); + }) + .then(() => { + observer.complete(); + }) + .catch((err: any) => { + handleError(err, observer); + }); + }); + }; +} diff --git a/src/utilities/types/OnlyRequiredProperties.ts b/src/utilities/types/OnlyRequiredProperties.ts new file mode 100644 index 00000000000..5264a0fca69 --- /dev/null +++ b/src/utilities/types/OnlyRequiredProperties.ts @@ -0,0 +1,6 @@ +/** + * Returns a new type that only contains the required properties from `T` + */ +export type OnlyRequiredProperties = { + [K in keyof T as {} extends Pick ? never : K]: T[K]; +}; diff --git a/tsdoc.json b/tsdoc.json new file mode 100644 index 00000000000..c49aafb4c6e --- /dev/null +++ b/tsdoc.json @@ -0,0 +1,24 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + + // Inherit the TSDoc configuration for API Extractor + "extends": ["@microsoft/api-extractor/extends/tsdoc-base.json"], + + "tagDefinitions": [ + { + "tagName": "@since", + "syntaxKind": "block", + "allowMultiple": false + }, + { + "tagName": "@docGroup", + "syntaxKind": "block", + "allowMultiple": false + } + ], + + "supportForTags": { + "@since": true, + "@docGroup": true + } +}