diff --git a/.api-reports/api-report-cache.md b/.api-reports/api-report-cache.md index 7eed64d3052..0f075697b78 100644 --- a/.api-reports/api-report-cache.md +++ b/.api-reports/api-report-cache.md @@ -8,9 +8,10 @@ import type { DocumentNode } from 'graphql'; import type { FieldNode } from 'graphql'; import type { FragmentDefinitionNode } from 'graphql'; import type { InlineFragmentNode } from 'graphql'; +import { Observable } from 'zen-observable-ts'; import type { SelectionSetNode } from 'graphql'; import { Trie } from '@wry/trie'; -import type { TypedDocumentNode } from '@graphql-typed-document-node/core'; +import { TypedDocumentNode } from '@graphql-typed-document-node/core'; // Warning: (ae-forgotten-export) The symbol "StoreObjectValueMaybeReference" needs to be exported by the entry point index.d.ts // @@ -63,6 +64,8 @@ export abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // (undocumented) abstract write(write: Cache_2.WriteOptions): Reference | undefined; // (undocumented) @@ -274,6 +277,40 @@ export interface DataProxy { writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } +// Warning: (ae-forgotten-export) The symbol "DeepPartialPrimitive" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlyMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialSet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlySet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartial = T extends DeepPartialPrimitive ? T : T extends Map ? DeepPartialMap : T extends ReadonlyMap ? DeepPartialReadonlyMap : T extends Set ? DeepPartialSet : T extends ReadonlySet ? DeepPartialReadonlySet : T extends (...args: any[]) => unknown ? T | undefined : T extends object ? T extends (ReadonlyArray) ? TItem[] extends (T) ? readonly TItem[] extends T ? ReadonlyArray> : Array> : DeepPartialObject : DeepPartialObject : unknown; + +// Warning: (ae-forgotten-export) The symbol "DeepPartial" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialMap = {} & Map, DeepPartial>; + +// @public (undocumented) +type DeepPartialObject = { + [K in keyof T]?: DeepPartial; +}; + +// Warning: (ae-forgotten-export) The symbol "Primitive" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialPrimitive = Primitive | Date | RegExp; + +// @public (undocumented) +type DeepPartialReadonlyMap = {} & ReadonlyMap, DeepPartial>; + +// @public (undocumented) +type DeepPartialReadonlySet = {} & ReadonlySet>; + +// @public (undocumented) +type DeepPartialSet = {} & Set>; + // Warning: (ae-forgotten-export) The symbol "KeyFieldsContext" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -533,8 +570,6 @@ export class InMemoryCache extends ApolloCache { protected broadcastWatches(options?: BroadcastOptions): void; // (undocumented) protected config: InMemoryCacheConfig; - // Warning: (ae-forgotten-export) The symbol "OperationVariables" needs to be exported by the entry point index.d.ts - // // (undocumented) diff(options: Cache_2.DiffOptions): Cache_2.DiffResult; // (undocumented) @@ -824,6 +859,9 @@ export type PossibleTypesMap = { [supertype: string]: string[]; }; +// @public (undocumented) +type Primitive = null | undefined | string | number | boolean | symbol | bigint; + // @public (undocumented) type ReactiveListener = (value: T) => any; @@ -936,6 +974,28 @@ export type TypePolicy = { }; }; +// @public +export interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +export type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) interface WriteContext extends ReadMergeModifyContext { // (undocumented) diff --git a/.api-reports/api-report-core.md b/.api-reports/api-report-core.md index b405231b1df..aabdc975485 100644 --- a/.api-reports/api-report-core.md +++ b/.api-reports/api-report-core.md @@ -79,6 +79,7 @@ export abstract class ApolloCache implements DataProxy { updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; + watchFragment(options: WatchFragmentOptions): Observable>; // (undocumented) abstract write(write: Cache_2.WriteOptions): Reference | undefined; // (undocumented) @@ -133,6 +134,7 @@ export class ApolloClient implements DataProxy { readonly typeDefs: ApolloClientOptions["typeDefs"]; // (undocumented) version: string; + watchFragment(options: WatchFragmentOptions): Observable>; watchQuery(options: WatchQueryOptions): ObservableQuery; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; @@ -520,6 +522,40 @@ export interface DataProxy { writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } +// Warning: (ae-forgotten-export) The symbol "DeepPartialPrimitive" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlyMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialSet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlySet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartial = T extends DeepPartialPrimitive ? T : T extends Map ? DeepPartialMap : T extends ReadonlyMap ? DeepPartialReadonlyMap : T extends Set ? DeepPartialSet : T extends ReadonlySet ? DeepPartialReadonlySet : T extends (...args: any[]) => unknown ? T | undefined : T extends object ? T extends (ReadonlyArray) ? TItem[] extends (T) ? readonly TItem[] extends T ? ReadonlyArray> : Array> : DeepPartialObject : DeepPartialObject : unknown; + +// Warning: (ae-forgotten-export) The symbol "DeepPartial" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialMap = {} & Map, DeepPartial>; + +// @public (undocumented) +type DeepPartialObject = { + [K in keyof T]?: DeepPartial; +}; + +// Warning: (ae-forgotten-export) The symbol "Primitive" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialPrimitive = Primitive | Date | RegExp; + +// @public (undocumented) +type DeepPartialReadonlyMap = {} & ReadonlyMap, DeepPartial>; + +// @public (undocumented) +type DeepPartialReadonlySet = {} & ReadonlySet>; + +// @public (undocumented) +type DeepPartialSet = {} & Set>; + // @public (undocumented) export interface DefaultContext extends Record { } @@ -1633,6 +1669,9 @@ export type PossibleTypesMap = { [supertype: string]: string[]; }; +// @public (undocumented) +type Primitive = null | undefined | string | number | boolean | symbol | bigint; + // @public (undocumented) const print_2: ((ast: ASTNode) => string) & { reset(): void; @@ -2171,6 +2210,28 @@ export interface UriFunction { (operation: Operation): string; } +// @public +export interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +export type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) export type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report-react.md b/.api-reports/api-report-react.md index a12089d47fc..c72fe91334c 100644 --- a/.api-reports/api-report-react.md +++ b/.api-reports/api-report-react.md @@ -80,6 +80,10 @@ abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentResult" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -155,7 +159,7 @@ class ApolloClient implements DataProxy { 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 + watchFragment(options: WatchFragmentOptions): Observable>; // 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 watchQuery(options: WatchQueryOptions): ObservableQuery; @@ -537,7 +541,7 @@ type ConcastSourcesIterable = Iterable>; export interface Context extends Record { } -// @alpha +// @public export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; // @public (undocumented) @@ -1753,7 +1757,6 @@ export interface QueryReference { // // @internal (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; - // @alpha toPromise(): Promise>; } @@ -2356,6 +2359,28 @@ TVariables variables: TVariables; }; +// @public +interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report-react_components.md b/.api-reports/api-report-react_components.md index 75bdf5cdc69..ff3554ad73e 100644 --- a/.api-reports/api-report-react_components.md +++ b/.api-reports/api-report-react_components.md @@ -80,6 +80,10 @@ abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentResult" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -156,7 +160,7 @@ class ApolloClient implements DataProxy { 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 + watchFragment(options: WatchFragmentOptions): Observable>; // 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 watchQuery(options: WatchQueryOptions): ObservableQuery; @@ -555,6 +559,40 @@ interface DataProxy { writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } +// Warning: (ae-forgotten-export) The symbol "DeepPartialPrimitive" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlyMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialSet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlySet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartial = T extends DeepPartialPrimitive ? T : T extends Map ? DeepPartialMap : T extends ReadonlyMap ? DeepPartialReadonlyMap : T extends Set ? DeepPartialSet : T extends ReadonlySet ? DeepPartialReadonlySet : T extends (...args: any[]) => unknown ? T | undefined : T extends object ? T extends (ReadonlyArray) ? TItem[] extends (T) ? readonly TItem[] extends T ? ReadonlyArray> : Array> : DeepPartialObject : DeepPartialObject : unknown; + +// Warning: (ae-forgotten-export) The symbol "DeepPartial" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialMap = {} & Map, DeepPartial>; + +// @public (undocumented) +type DeepPartialObject = { + [K in keyof T]?: DeepPartial; +}; + +// Warning: (ae-forgotten-export) The symbol "Primitive" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialPrimitive = Primitive | Date | RegExp; + +// @public (undocumented) +type DeepPartialReadonlyMap = {} & ReadonlyMap, DeepPartial>; + +// @public (undocumented) +type DeepPartialReadonlySet = {} & ReadonlySet>; + +// @public (undocumented) +type DeepPartialSet = {} & Set>; + // @public (undocumented) interface DefaultContext extends Record { } @@ -1192,6 +1230,9 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Primitive = null | undefined | string | number | boolean | symbol | bigint; + // @public @deprecated (undocumented) export function Query(props: QueryComponentOptions): ReactTypes.JSX.Element | null; @@ -1699,6 +1740,28 @@ interface UriFunction { (operation: Operation): string; } +// @public +interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report-react_context.md b/.api-reports/api-report-react_context.md index 89bdcc1f98d..57b75b6f9eb 100644 --- a/.api-reports/api-report-react_context.md +++ b/.api-reports/api-report-react_context.md @@ -79,6 +79,10 @@ abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentResult" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -155,7 +159,7 @@ class ApolloClient implements DataProxy { 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 + watchFragment(options: WatchFragmentOptions): Observable>; // 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 watchQuery(options: WatchQueryOptions): ObservableQuery; @@ -553,6 +557,40 @@ interface DataProxy { writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } +// Warning: (ae-forgotten-export) The symbol "DeepPartialPrimitive" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlyMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialSet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlySet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartial = T extends DeepPartialPrimitive ? T : T extends Map ? DeepPartialMap : T extends ReadonlyMap ? DeepPartialReadonlyMap : T extends Set ? DeepPartialSet : T extends ReadonlySet ? DeepPartialReadonlySet : T extends (...args: any[]) => unknown ? T | undefined : T extends object ? T extends (ReadonlyArray) ? TItem[] extends (T) ? readonly TItem[] extends T ? ReadonlyArray> : Array> : DeepPartialObject : DeepPartialObject : unknown; + +// Warning: (ae-forgotten-export) The symbol "DeepPartial" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialMap = {} & Map, DeepPartial>; + +// @public (undocumented) +type DeepPartialObject = { + [K in keyof T]?: DeepPartial; +}; + +// Warning: (ae-forgotten-export) The symbol "Primitive" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialPrimitive = Primitive | Date | RegExp; + +// @public (undocumented) +type DeepPartialReadonlyMap = {} & ReadonlyMap, DeepPartial>; + +// @public (undocumented) +type DeepPartialReadonlySet = {} & ReadonlySet>; + +// @public (undocumented) +type DeepPartialSet = {} & Set>; + // @public (undocumented) interface DefaultContext extends Record { } @@ -1131,6 +1169,9 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Primitive = null | undefined | string | number | boolean | symbol | bigint; + // @public (undocumented) interface QueryData { // (undocumented) @@ -1622,6 +1663,28 @@ interface UriFunction { (operation: Operation): string; } +// @public +interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report-react_hoc.md b/.api-reports/api-report-react_hoc.md index 2a2c95db36a..671fa20ca12 100644 --- a/.api-reports/api-report-react_hoc.md +++ b/.api-reports/api-report-react_hoc.md @@ -79,6 +79,10 @@ abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentResult" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -155,7 +159,7 @@ class ApolloClient implements DataProxy { 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 + watchFragment(options: WatchFragmentOptions): Observable>; // 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 watchQuery(options: WatchQueryOptions): ObservableQuery; @@ -551,6 +555,40 @@ interface DataProxy { // @public (undocumented) export type DataValue = QueryControls & Partial; +// Warning: (ae-forgotten-export) The symbol "DeepPartialPrimitive" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlyMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialSet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlySet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartial = T extends DeepPartialPrimitive ? T : T extends Map ? DeepPartialMap : T extends ReadonlyMap ? DeepPartialReadonlyMap : T extends Set ? DeepPartialSet : T extends ReadonlySet ? DeepPartialReadonlySet : T extends (...args: any[]) => unknown ? T | undefined : T extends object ? T extends (ReadonlyArray) ? TItem[] extends (T) ? readonly TItem[] extends T ? ReadonlyArray> : Array> : DeepPartialObject : DeepPartialObject : unknown; + +// Warning: (ae-forgotten-export) The symbol "DeepPartial" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialMap = {} & Map, DeepPartial>; + +// @public (undocumented) +type DeepPartialObject = { + [K in keyof T]?: DeepPartial; +}; + +// Warning: (ae-forgotten-export) The symbol "Primitive" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialPrimitive = Primitive | Date | RegExp; + +// @public (undocumented) +type DeepPartialReadonlyMap = {} & ReadonlyMap, DeepPartial>; + +// @public (undocumented) +type DeepPartialReadonlySet = {} & ReadonlySet>; + +// @public (undocumented) +type DeepPartialSet = {} & Set>; + // @public (undocumented) interface DefaultContext extends Record { } @@ -1178,6 +1216,9 @@ export interface OptionProps; +// @public (undocumented) +type Primitive = null | undefined | string | number | boolean | symbol | bigint; + // @public (undocumented) export interface QueryControls { // (undocumented) @@ -1634,6 +1675,28 @@ interface UriFunction { (operation: Operation): string; } +// @public +interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report-react_hooks.md b/.api-reports/api-report-react_hooks.md index 8e157061ac6..ddd545d3c7e 100644 --- a/.api-reports/api-report-react_hooks.md +++ b/.api-reports/api-report-react_hooks.md @@ -78,6 +78,10 @@ abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentResult" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -154,7 +158,7 @@ class ApolloClient implements DataProxy { 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 + watchFragment(options: WatchFragmentOptions): Observable>; // 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 watchQuery(options: WatchQueryOptions): ObservableQuery; @@ -1628,7 +1632,6 @@ interface QueryReference { // // @internal (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; - // @alpha toPromise(): Promise>; } @@ -2192,6 +2195,28 @@ export interface UseSuspenseQueryResult; } +// @public +interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report-react_internal.md b/.api-reports/api-report-react_internal.md index 86141628ec6..0be4d2b9cb8 100644 --- a/.api-reports/api-report-react_internal.md +++ b/.api-reports/api-report-react_internal.md @@ -78,6 +78,10 @@ abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentResult" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -154,7 +158,7 @@ class ApolloClient implements DataProxy { 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 + watchFragment(options: WatchFragmentOptions): Observable>; // 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 watchQuery(options: WatchQueryOptions): ObservableQuery; @@ -1514,7 +1518,6 @@ export interface QueryReference { [PROMISE_SYMBOL]: QueryRefPromise; // @internal (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; - // @alpha toPromise(): Promise>; } @@ -1933,6 +1936,17 @@ type UseFragmentResult = { // @public function useQuery(query: DocumentNode | TypedDocumentNode, options?: QueryHookOptions, NoInfer>): QueryResult; +// Warning: (ae-forgotten-export) The symbol "UseQueryRefHandlersResult" needs to be exported by the entry point index.d.ts +// +// @public +function useQueryRefHandlers(queryRef: QueryReference): UseQueryRefHandlersResult; + +// @public (undocumented) +interface UseQueryRefHandlersResult { + fetchMore: FetchMoreFunction; + refetch: RefetchFunction; +} + // Warning: (ae-forgotten-export) The symbol "UseReadQueryResult" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -2009,6 +2023,28 @@ interface UseSuspenseQueryResult; } +// @public +interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; @@ -2031,6 +2067,10 @@ interface WrappableHooks { // // (undocumented) useQuery: typeof useQuery; + // Warning: (ae-forgotten-export) The symbol "useQueryRefHandlers" needs to be exported by the entry point index.d.ts + // + // (undocumented) + useQueryRefHandlers: typeof useQueryRefHandlers; // Warning: (ae-forgotten-export) The symbol "useReadQuery" needs to be exported by the entry point index.d.ts // // (undocumented) diff --git a/.api-reports/api-report-react_ssr.md b/.api-reports/api-report-react_ssr.md index 5a8d6bfde63..7b183853465 100644 --- a/.api-reports/api-report-react_ssr.md +++ b/.api-reports/api-report-react_ssr.md @@ -79,6 +79,10 @@ abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentResult" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -155,7 +159,7 @@ class ApolloClient implements DataProxy { 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 + watchFragment(options: WatchFragmentOptions): Observable>; // 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 watchQuery(options: WatchQueryOptions): ObservableQuery; @@ -522,6 +526,40 @@ interface DataProxy { writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } +// Warning: (ae-forgotten-export) The symbol "DeepPartialPrimitive" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlyMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialSet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlySet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartial = T extends DeepPartialPrimitive ? T : T extends Map ? DeepPartialMap : T extends ReadonlyMap ? DeepPartialReadonlyMap : T extends Set ? DeepPartialSet : T extends ReadonlySet ? DeepPartialReadonlySet : T extends (...args: any[]) => unknown ? T | undefined : T extends object ? T extends (ReadonlyArray) ? TItem[] extends (T) ? readonly TItem[] extends T ? ReadonlyArray> : Array> : DeepPartialObject : DeepPartialObject : unknown; + +// Warning: (ae-forgotten-export) The symbol "DeepPartial" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialMap = {} & Map, DeepPartial>; + +// @public (undocumented) +type DeepPartialObject = { + [K in keyof T]?: DeepPartial; +}; + +// Warning: (ae-forgotten-export) The symbol "Primitive" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialPrimitive = Primitive | Date | RegExp; + +// @public (undocumented) +type DeepPartialReadonlyMap = {} & ReadonlyMap, DeepPartial>; + +// @public (undocumented) +type DeepPartialReadonlySet = {} & ReadonlySet>; + +// @public (undocumented) +type DeepPartialSet = {} & Set>; + // @public (undocumented) interface DefaultContext extends Record { } @@ -1116,6 +1154,9 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Primitive = null | undefined | string | number | boolean | symbol | bigint; + // @public (undocumented) interface QueryData { // (undocumented) @@ -1607,6 +1648,28 @@ interface UriFunction { (operation: Operation): string; } +// @public +interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report-testing.md b/.api-reports/api-report-testing.md index c02d6cfd1ec..2c1e6dd0195 100644 --- a/.api-reports/api-report-testing.md +++ b/.api-reports/api-report-testing.md @@ -79,6 +79,10 @@ abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentResult" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -155,7 +159,7 @@ class ApolloClient implements DataProxy { 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 + watchFragment(options: WatchFragmentOptions): Observable>; // 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 watchQuery(options: WatchQueryOptions): ObservableQuery; @@ -518,6 +522,40 @@ interface DataProxy { writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } +// Warning: (ae-forgotten-export) The symbol "DeepPartialPrimitive" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlyMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialSet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlySet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartial = T extends DeepPartialPrimitive ? T : T extends Map ? DeepPartialMap : T extends ReadonlyMap ? DeepPartialReadonlyMap : T extends Set ? DeepPartialSet : T extends ReadonlySet ? DeepPartialReadonlySet : T extends (...args: any[]) => unknown ? T | undefined : T extends object ? T extends (ReadonlyArray) ? TItem[] extends (T) ? readonly TItem[] extends T ? ReadonlyArray> : Array> : DeepPartialObject : DeepPartialObject : unknown; + +// Warning: (ae-forgotten-export) The symbol "DeepPartial" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialMap = {} & Map, DeepPartial>; + +// @public (undocumented) +type DeepPartialObject = { + [K in keyof T]?: DeepPartial; +}; + +// Warning: (ae-forgotten-export) The symbol "Primitive" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialPrimitive = Primitive | Date | RegExp; + +// @public (undocumented) +type DeepPartialReadonlyMap = {} & ReadonlyMap, DeepPartial>; + +// @public (undocumented) +type DeepPartialReadonlySet = {} & ReadonlySet>; + +// @public (undocumented) +type DeepPartialSet = {} & Set>; + // @public (undocumented) interface DefaultContext extends Record { } @@ -1224,6 +1262,9 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Primitive = null | undefined | string | number | boolean | symbol | bigint; + // @public (undocumented) class QueryInfo { constructor(queryManager: QueryManager, queryId?: string); @@ -1659,6 +1700,28 @@ type VariableMatcher> = (variables: V) => boolean; // @public (undocumented) export function wait(ms: number): Promise; +// @public +interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report-testing_core.md b/.api-reports/api-report-testing_core.md index 2a3357aa0e2..da8706e0df2 100644 --- a/.api-reports/api-report-testing_core.md +++ b/.api-reports/api-report-testing_core.md @@ -78,6 +78,10 @@ abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentResult" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // Warning: (ae-forgotten-export) The symbol "Reference" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -154,7 +158,7 @@ class ApolloClient implements DataProxy { 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 + watchFragment(options: WatchFragmentOptions): Observable>; // 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 watchQuery(options: WatchQueryOptions): ObservableQuery; @@ -517,6 +521,40 @@ interface DataProxy { writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; } +// Warning: (ae-forgotten-export) The symbol "DeepPartialPrimitive" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlyMap" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialSet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialReadonlySet" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "DeepPartialObject" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartial = T extends DeepPartialPrimitive ? T : T extends Map ? DeepPartialMap : T extends ReadonlyMap ? DeepPartialReadonlyMap : T extends Set ? DeepPartialSet : T extends ReadonlySet ? DeepPartialReadonlySet : T extends (...args: any[]) => unknown ? T | undefined : T extends object ? T extends (ReadonlyArray) ? TItem[] extends (T) ? readonly TItem[] extends T ? ReadonlyArray> : Array> : DeepPartialObject : DeepPartialObject : unknown; + +// Warning: (ae-forgotten-export) The symbol "DeepPartial" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialMap = {} & Map, DeepPartial>; + +// @public (undocumented) +type DeepPartialObject = { + [K in keyof T]?: DeepPartial; +}; + +// Warning: (ae-forgotten-export) The symbol "Primitive" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type DeepPartialPrimitive = Primitive | Date | RegExp; + +// @public (undocumented) +type DeepPartialReadonlyMap = {} & ReadonlyMap, DeepPartial>; + +// @public (undocumented) +type DeepPartialReadonlySet = {} & ReadonlySet>; + +// @public (undocumented) +type DeepPartialSet = {} & Set>; + // @public (undocumented) interface DefaultContext extends Record { } @@ -1179,6 +1217,9 @@ type OperationVariables = Record; // @public (undocumented) type Path = ReadonlyArray; +// @public (undocumented) +type Primitive = null | undefined | string | number | boolean | symbol | bigint; + // @public (undocumented) class QueryInfo { constructor(queryManager: QueryManager, queryId?: string); @@ -1616,6 +1657,28 @@ type VariableMatcher> = (variables: V) => boolean; // @public (undocumented) export function wait(ms: number): Promise; +// @public +interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report-testing_experimental.md b/.api-reports/api-report-testing_experimental.md new file mode 100644 index 00000000000..caeebb6e726 --- /dev/null +++ b/.api-reports/api-report-testing_experimental.md @@ -0,0 +1,89 @@ +## 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 { FieldNode } from 'graphql'; +import type { FragmentDefinitionNode } from 'graphql'; +import type { GraphQLSchema } from 'graphql'; + +// @alpha +export const createSchemaFetch: (schema: GraphQLSchema, mockFetchOpts?: { + validate?: boolean; + delay?: { + min: number; + max: number; + }; +}) => ((uri?: any, options?: any) => Promise) & { + mockGlobal: () => { + restore: () => void; + } & Disposable; +}; + +// Warning: (ae-forgotten-export) The symbol "TestSchemaOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "ProxiedSchema" needs to be exported by the entry point index.d.ts +// +// @alpha +export const createTestSchema: (schemaWithTypeDefs: GraphQLSchema, options: TestSchemaOptions) => ProxiedSchema; + +// @public +interface FragmentMap { + // (undocumented) + [fragmentName: string]: FragmentDefinitionNode; +} + +// Warning: (ae-forgotten-export) The symbol "TestSchemaFns" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type ProxiedSchema = GraphQLSchema & TestSchemaFns; + +// @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) +interface TestSchemaFns { + // (undocumented) + add: (addOptions: { + resolvers: Resolvers; + }) => ProxiedSchema; + // (undocumented) + fork: (forkOptions?: { + resolvers?: Resolvers; + }) => ProxiedSchema; + // (undocumented) + reset: () => void; +} + +// @public (undocumented) +interface TestSchemaOptions { + // (undocumented) + resolvers: Resolvers; + // (undocumented) + scalars?: { + [key: string]: any; + }; +} + +// Warnings were encountered during analysis: +// +// 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/types.ts:203:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/testing/experimental/createTestSchema.ts:10:23 - (ae-forgotten-export) The symbol "Resolvers" 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 02656941096..9d573f7a59c 100644 --- a/.api-reports/api-report-utilities.md +++ b/.api-reports/api-report-utilities.md @@ -94,6 +94,10 @@ abstract class ApolloCache implements DataProxy { 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 "OperationVariables" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentOptions" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "WatchFragmentResult" needs to be exported by the entry point index.d.ts + watchFragment(options: WatchFragmentOptions): Observable>; // (undocumented) abstract write(write: Cache_2.WriteOptions): Reference | undefined; // (undocumented) @@ -167,7 +171,7 @@ class ApolloClient implements DataProxy { 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 + watchFragment(options: WatchFragmentOptions): Observable>; // 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 watchQuery(options: WatchQueryOptions): ObservableQuery; @@ -2565,6 +2569,28 @@ export function valueToObjectRepresentation(argObj: any, name: NameNode, value: // @public (undocumented) export type VariableValue = (node: VariableNode) => any; +// @public +interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.api-reports/api-report.md b/.api-reports/api-report.md index 6d1e721d379..e9290fed8eb 100644 --- a/.api-reports/api-report.md +++ b/.api-reports/api-report.md @@ -81,6 +81,7 @@ export abstract class ApolloCache implements DataProxy { updateQuery(options: Cache_2.UpdateQueryOptions, update: (data: TData | null) => TData | null | void): TData | null; // (undocumented) abstract watch(watch: Cache_2.WatchOptions): () => void; + watchFragment(options: WatchFragmentOptions): Observable>; // (undocumented) abstract write(write: Cache_2.WriteOptions): Reference | undefined; // (undocumented) @@ -135,6 +136,7 @@ export class ApolloClient implements DataProxy { readonly typeDefs: ApolloClientOptions["typeDefs"]; // (undocumented) version: string; + watchFragment(options: WatchFragmentOptions): Observable>; watchQuery(options: WatchQueryOptions): ObservableQuery; writeFragment(options: DataProxy.WriteFragmentOptions): Reference | undefined; writeQuery(options: DataProxy.WriteQueryOptions): Reference | undefined; @@ -541,7 +543,7 @@ export const concat: typeof ApolloLink.concat; // @public (undocumented) export const createHttpLink: (linkOptions?: HttpOptions) => ApolloLink; -// @alpha +// @public export function createQueryPreloader(client: ApolloClient): PreloadQueryFunction; // @public @deprecated (undocumented) @@ -2326,7 +2328,6 @@ export interface QueryReference { // // @internal (undocumented) readonly [QUERY_REFERENCE_SYMBOL]: InternalQueryReference; - // @alpha toPromise(): Promise>; } @@ -3019,6 +3020,28 @@ TVariables variables: TVariables; }; +// @public +export interface WatchFragmentOptions { + // @deprecated (undocumented) + canonizeResults?: boolean; + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: StoreObject | Reference | string; + optimistic?: boolean; + variables?: TVars; +} + +// @public +export type WatchFragmentResult = { + data: TData; + complete: true; + missing?: never; +} | { + data: DeepPartial; + complete: false; + missing: MissingTree; +}; + // @public (undocumented) export type WatchQueryFetchPolicy = FetchPolicy | "cache-and-network"; diff --git a/.changeset/cuddly-emus-fail.md b/.changeset/cuddly-emus-fail.md new file mode 100644 index 00000000000..d3830efd702 --- /dev/null +++ b/.changeset/cuddly-emus-fail.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +AutoCleanedCache: only schedule batched cache cleanup if the cache is full (fixes #11790) diff --git a/.changeset/healthy-chairs-sleep.md b/.changeset/healthy-chairs-sleep.md new file mode 100644 index 00000000000..cfaf23a0a96 --- /dev/null +++ b/.changeset/healthy-chairs-sleep.md @@ -0,0 +1,5 @@ +--- +"@apollo/client": patch +--- + +Update the `rehackt` dependency to `^0.1.0` diff --git a/.github/workflows/snapshot-release.yml b/.github/workflows/snapshot-release.yml index 04a7795fcad..6020dddee7d 100644 --- a/.github/workflows/snapshot-release.yml +++ b/.github/workflows/snapshot-release.yml @@ -33,13 +33,44 @@ jobs: - uses: alessbell/pull-request-comment-branch@v2.1.0 id: comment-branch - - name: Checkout head ref + - name: Get sha + id: parse-sha + continue-on-error: true + run: | + if [ "${{ steps.comment-branch.outputs.head_owner }}" == "apollographql" ]; then + echo "sha=${{ steps.comment-branch.outputs.head_sha }}" >> "${GITHUB_OUTPUT}" + else + sha_from_comment="$(echo $COMMENT_BODY | tr -s ' ' | cut -d ' ' -f2)" + + if [ $sha_from_comment == "/release:pr" ]; then + exit 1 + else + echo "sha=$sha_from_comment" >> "${GITHUB_OUTPUT}" + fi + fi + env: + COMMENT_BODY: ${{ github.event.comment.body }} + + - name: Comment sha reminder + if: steps.parse-sha.outcome == 'failure' + uses: peter-evans/create-or-update-comment@v2.1.0 + with: + issue-number: ${{ github.event.issue.number }} + body: | + Did you forget to add the sha? Please use `/release:pr ` + + - name: Fail job + if: steps.parse-sha.outcome == 'failure' + run: | + exit 1 + + - name: Checkout ref uses: actions/checkout@v4 with: ## specify the owner + repository in order to checkout the fork ## for community PRs repository: ${{ steps.comment-branch.outputs.head_owner }}/${{ steps.comment-branch.outputs.head_repo }} - ref: ${{ steps.comment-branch.outputs.head_ref }} + ref: ${{ steps.parse-sha.outputs.sha }} fetch-depth: 0 - name: Detect new changesets @@ -47,7 +78,7 @@ jobs: run: | delimiter="$(openssl rand -hex 8)" echo "changesets<<${delimiter}" >> "${GITHUB_OUTPUT}" - echo "$(git diff --name-only --diff-filter=A ${{ steps.comment-branch.outputs.base_sha }} ${{ steps.comment-branch.outputs.head_sha }} .changeset/*.md)" >> "${GITHUB_OUTPUT}" + echo "$(git diff --name-only --diff-filter=A ${{ steps.comment-branch.outputs.base_sha }} ${{ steps.parse-sha.outputs.sha }} .changeset/*.md)" >> "${GITHUB_OUTPUT}" echo "${delimiter}" >> "${GITHUB_OUTPUT}" - name: Append NPM token to .npmrc diff --git a/.prettierignore b/.prettierignore index fe391b018fc..4af59c9d031 100644 --- a/.prettierignore +++ b/.prettierignore @@ -24,6 +24,7 @@ !/docs/source/development-testing /docs/source/development-testing/** !/docs/source/development-testing/reducing-bundle-size.mdx +!/docs/source/development-testing/schema-driven-testing.mdx !docs/shared /docs/shared/** diff --git a/.size-limits.json b/.size-limits.json index b33df04bf24..b18103fdaec 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 39373, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32636 + "dist/apollo-client.min.cjs": 39551, + "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 32826 } diff --git a/.vscode/launch.json b/.vscode/launch.json index 6cdb71bc99d..d40a6ff9315 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,25 +1,22 @@ { "version": "0.2.0", "configurations": [ - { - "name": "Attach to Node.js inspector", - "port": 9229, - "request": "attach", - "skipFiles": ["/**"], - "type": "pwa-node" - }, { "type": "node", "request": "launch", - "name": "Jest Current File", - "program": "${workspaceFolder}/node_modules/.bin/jest", - "args": ["${relativeFile}", "--config", "./config/jest.config.js"], + "name": "Jest Attach Node Inspector for Current File", + "cwd": "${workspaceFolder}", + "runtimeArgs": [ + "--inspect-brk", + "${workspaceRoot}/node_modules/.bin/jest", + "${relativeFile}", + "--config", + "./config/jest.config.js", + "--runInBand", + "--watch" + ], "console": "integratedTerminal", - "internalConsoleOptions": "neverOpen", - "disableOptimisticBPs": true, - "windows": { - "program": "${workspaceFolder}/node_modules/jest/bin/jest" - } + "internalConsoleOptions": "neverOpen" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 66cd000df03..29faf90bc17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,77 @@ # @apollo/client +## 3.10.0 + +### Minor Changes + +- [#11605](https://github.com/apollographql/apollo-client/pull/11605) [`e2dd4c9`](https://github.com/apollographql/apollo-client/commit/e2dd4c95290cea604b548cc446826d89aafe8e11) Thanks [@alessbell](https://github.com/alessbell)! - Adds `createMockFetch` utility for integration testing that includes the link chain + +- [#11760](https://github.com/apollographql/apollo-client/pull/11760) [`acd1982`](https://github.com/apollographql/apollo-client/commit/acd1982a59ed66fc44fa9e70b08a31c69dac35a6) Thanks [@alessbell](https://github.com/alessbell)! - `createTestSchema` now uses graphql-tools `mergeResolvers` to merge resolvers instead of a shallow merge. + +- [#11764](https://github.com/apollographql/apollo-client/pull/11764) [`f046aa9`](https://github.com/apollographql/apollo-client/commit/f046aa9fc24ac197a797045d280811a3bbe05806) Thanks [@alessbell](https://github.com/alessbell)! - Rename `createProxiedSchema` to `createTestSchema` and `createMockFetch` to `createSchemaFetch`. + +- [#11777](https://github.com/apollographql/apollo-client/pull/11777) [`5dfc79f`](https://github.com/apollographql/apollo-client/commit/5dfc79fa6d974362f38361f7dffbe984a9546377) Thanks [@alessbell](https://github.com/alessbell)! - Call `createMockSchema` inside `createTestSchema`. + +- [#11774](https://github.com/apollographql/apollo-client/pull/11774) [`2583488`](https://github.com/apollographql/apollo-client/commit/2583488677912cb4500e5fb9e3f91b5c113c4cdb) Thanks [@alessbell](https://github.com/alessbell)! - Add ability to set min and max delay in `createSchemaFetch` + +- [#11605](https://github.com/apollographql/apollo-client/pull/11605) [`e2dd4c9`](https://github.com/apollographql/apollo-client/commit/e2dd4c95290cea604b548cc446826d89aafe8e11) Thanks [@alessbell](https://github.com/alessbell)! - Adds proxiedSchema and createMockSchema testing utilities + +- [#11465](https://github.com/apollographql/apollo-client/pull/11465) [`7623da7`](https://github.com/apollographql/apollo-client/commit/7623da7720855b0c19e13ff9124679f426a39725) Thanks [@alessbell](https://github.com/alessbell)! - Add `watchFragment` method to the cache and expose it on ApolloClient, refactor `useFragment` using `watchFragment`. + +- [#11743](https://github.com/apollographql/apollo-client/pull/11743) [`78891f9`](https://github.com/apollographql/apollo-client/commit/78891f9ec81c0b7a7e010f5550a91965fa33a958) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Remove alpha designation for `queryRef.toPromise()` to stabilize the API. + +- [#11743](https://github.com/apollographql/apollo-client/pull/11743) [`78891f9`](https://github.com/apollographql/apollo-client/commit/78891f9ec81c0b7a7e010f5550a91965fa33a958) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Remove alpha designation for `createQueryPreloader` to stabilize the API. + +- [#11783](https://github.com/apollographql/apollo-client/pull/11783) [`440563a`](https://github.com/apollographql/apollo-client/commit/440563ab2c47efcb9c7d08f52531ade33d753037) Thanks [@alessbell](https://github.com/alessbell)! - Moves new testing utilities to their own entrypoint, `testing/experimental` + +### Patch Changes + +- [#11757](https://github.com/apollographql/apollo-client/pull/11757) [`9825295`](https://github.com/apollographql/apollo-client/commit/982529530893f66a1d236f0fff53862e513fc9a8) Thanks [@phryneas](https://github.com/phryneas)! - Adjust `useReadQuery` wrapper logic to work with transported objects. + +- [#11771](https://github.com/apollographql/apollo-client/pull/11771) [`e72cbba`](https://github.com/apollographql/apollo-client/commit/e72cbba07e5caa6d75b44ca8c766846e855a6c93) Thanks [@phryneas](https://github.com/phryneas)! - Wrap `useQueryRefHandlers` in `wrapHook`. + +- [#11754](https://github.com/apollographql/apollo-client/pull/11754) [`80d2ba5`](https://github.com/apollographql/apollo-client/commit/80d2ba579fe6d2a2d102d1fe79d7d503f31cd931) Thanks [@alessbell](https://github.com/alessbell)! - Export `WatchFragmentOptions` and `WatchFragmentResult` from main entrypoint and fix bug where `this` wasn't bound to the `watchFragment` method on `ApolloClient`. + +## 3.10.0-rc.1 + +### Minor Changes + +- [#11760](https://github.com/apollographql/apollo-client/pull/11760) [`acd1982`](https://github.com/apollographql/apollo-client/commit/acd1982a59ed66fc44fa9e70b08a31c69dac35a6) Thanks [@alessbell](https://github.com/alessbell)! - `createTestSchema` now uses graphql-tools `mergeResolvers` to merge resolvers instead of a shallow merge. + +- [#11764](https://github.com/apollographql/apollo-client/pull/11764) [`f046aa9`](https://github.com/apollographql/apollo-client/commit/f046aa9fc24ac197a797045d280811a3bbe05806) Thanks [@alessbell](https://github.com/alessbell)! - Rename `createProxiedSchema` to `createTestSchema` and `createMockFetch` to `createSchemaFetch`. + +- [#11777](https://github.com/apollographql/apollo-client/pull/11777) [`5dfc79f`](https://github.com/apollographql/apollo-client/commit/5dfc79fa6d974362f38361f7dffbe984a9546377) Thanks [@alessbell](https://github.com/alessbell)! - Call `createMockSchema` inside `createTestSchema`. + +- [#11774](https://github.com/apollographql/apollo-client/pull/11774) [`2583488`](https://github.com/apollographql/apollo-client/commit/2583488677912cb4500e5fb9e3f91b5c113c4cdb) Thanks [@alessbell](https://github.com/alessbell)! - Add ability to set min and max delay in `createSchemaFetch` + +- [#11783](https://github.com/apollographql/apollo-client/pull/11783) [`440563a`](https://github.com/apollographql/apollo-client/commit/440563ab2c47efcb9c7d08f52531ade33d753037) Thanks [@alessbell](https://github.com/alessbell)! - Moves new testing utilities to their own entrypoint, `testing/experimental` + +### Patch Changes + +- [#11757](https://github.com/apollographql/apollo-client/pull/11757) [`9825295`](https://github.com/apollographql/apollo-client/commit/982529530893f66a1d236f0fff53862e513fc9a8) Thanks [@phryneas](https://github.com/phryneas)! - Adjust `useReadQuery` wrapper logic to work with transported objects. + +- [#11771](https://github.com/apollographql/apollo-client/pull/11771) [`e72cbba`](https://github.com/apollographql/apollo-client/commit/e72cbba07e5caa6d75b44ca8c766846e855a6c93) Thanks [@phryneas](https://github.com/phryneas)! - Wrap `useQueryRefHandlers` in `wrapHook`. + +- [#11754](https://github.com/apollographql/apollo-client/pull/11754) [`80d2ba5`](https://github.com/apollographql/apollo-client/commit/80d2ba579fe6d2a2d102d1fe79d7d503f31cd931) Thanks [@alessbell](https://github.com/alessbell)! - Export `WatchFragmentOptions` and `WatchFragmentResult` from main entrypoint and fix bug where `this` wasn't bound to the `watchFragment` method on `ApolloClient`. + +## 3.10.0-rc.0 + +### Minor Changes + +- [#11605](https://github.com/apollographql/apollo-client/pull/11605) [`e2dd4c9`](https://github.com/apollographql/apollo-client/commit/e2dd4c95290cea604b548cc446826d89aafe8e11) Thanks [@alessbell](https://github.com/alessbell)! - Adds `createMockFetch` utility for integration testing that includes the link chain + +- [#11605](https://github.com/apollographql/apollo-client/pull/11605) [`e2dd4c9`](https://github.com/apollographql/apollo-client/commit/e2dd4c95290cea604b548cc446826d89aafe8e11) Thanks [@alessbell](https://github.com/alessbell)! - Adds proxiedSchema and createMockSchema testing utilities + +- [#11743](https://github.com/apollographql/apollo-client/pull/11743) [`78891f9`](https://github.com/apollographql/apollo-client/commit/78891f9ec81c0b7a7e010f5550a91965fa33a958) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Remove alpha designation for `queryRef.toPromise()` to stabilize the API. + +- [#11743](https://github.com/apollographql/apollo-client/pull/11743) [`78891f9`](https://github.com/apollographql/apollo-client/commit/78891f9ec81c0b7a7e010f5550a91965fa33a958) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Remove alpha designation for `createQueryPreloader` to stabilize the API. + +## 3.10.0-alpha.0 + +### Minor Changes + +- [#11465](https://github.com/apollographql/apollo-client/pull/11465) [`7623da7`](https://github.com/apollographql/apollo-client/commit/7623da7720855b0c19e13ff9124679f426a39725) Thanks [@alessbell](https://github.com/alessbell)! - Add `watchFragment` method to the cache and expose it on ApolloClient, refactor `useFragment` using `watchFragment`. + ## 3.9.11 ### Patch Changes diff --git a/ROADMAP.md b/ROADMAP.md index d9b417ac323..fd4fe2dfdf8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 🔮 Apollo Client Roadmap -**Last updated: 2024-04-15** +**Last updated: 2024-04-22** For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md). @@ -13,14 +13,13 @@ For up to date release notes, refer to the project's [Changelog](https://github. --- -## [3.10.0](https://github.com/apollographql/apollo-client/milestone/33) - April 18th, 2024 +## [3.10.0](https://github.com/apollographql/apollo-client/milestone/33) - April 24th, 2024 -- RC target: April 2nd, 2024 +- Core `watchFragment` API to provide `useFragment`-like functionality for non-React envs +- schema-driven testing utilities ## Upcoming features -- Core `watchFragment` API to provide `useFragment`-like functionality for non-React envs -- schema-driven testing utilities - Data masking - Introduce a suspenseful `useFragment` that will suspend when the data is not yet loaded - leaner client (under alternate entry point) diff --git a/config/FixJSDOMEnvironment.js b/config/FixJSDOMEnvironment.js new file mode 100644 index 00000000000..dbbf9d3356f --- /dev/null +++ b/config/FixJSDOMEnvironment.js @@ -0,0 +1,21 @@ +const { default: JSDOMEnvironment } = require("jest-environment-jsdom"); + +// https://github.com/facebook/jest/blob/v29.4.3/website/versioned_docs/version-29.4/Configuration.md#testenvironment-string +class FixJSDOMEnvironment extends JSDOMEnvironment { + constructor(...args) { + super(...args); + + // FIXME https://github.com/jsdom/jsdom/issues/1724 + this.global.Headers = Headers; + this.global.Request = Request; + this.global.Response = Response; + + // FIXME: setting a global fetch breaks HttpLink tests + // and setting AbortController breaks PersistedQueryLink tests, which may + // indicate a memory leak + // this.global.fetch = fetch; + this.global.AbortController = AbortController; + } +} + +module.exports = FixJSDOMEnvironment; diff --git a/config/entryPoints.js b/config/entryPoints.js index cad194d61aa..674e1cc9ba2 100644 --- a/config/entryPoints.js +++ b/config/entryPoints.js @@ -27,6 +27,7 @@ const entryPoints = [ { dirs: ["react", "ssr"] }, { dirs: ["testing"], extensions: [".js", ".jsx"] }, { dirs: ["testing", "core"] }, + { dirs: ["testing", "experimental"] }, { dirs: ["utilities"] }, { dirs: ["utilities", "subscriptions", "relay"] }, { dirs: ["utilities", "subscriptions", "urql"] }, diff --git a/config/inlineInheritDoc.ts b/config/inlineInheritDoc.ts index 704054f28ec..0bb126629ae 100644 --- a/config/inlineInheritDoc.ts +++ b/config/inlineInheritDoc.ts @@ -143,6 +143,7 @@ function processComments() { if ( Node.isPropertySignature(node) || Node.isMethodSignature(node) || + Node.isMethodDeclaration(node) || Node.isCallSignatureDeclaration(node) ) { const docsNode = node.getJsDocs()[0]; diff --git a/config/jest.config.js b/config/jest.config.js index 6851e2a6e06..646185e63da 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -1,7 +1,7 @@ const defaults = { rootDir: "src", preset: "ts-jest", - testEnvironment: "jsdom", + testEnvironment: require.resolve("./FixJSDOMEnvironment.js"), setupFilesAfterEnv: ["/config/jest/setup.ts"], globals: { __DEV__: true, @@ -33,6 +33,7 @@ const react17TestFileIgnoreList = [ ignoreTSFiles, // We only support Suspense with React 18, so don't test suspense hooks with // React 17 + "src/testing/experimental/__tests__/createTestSchema.test.tsx", "src/react/hooks/__tests__/useSuspenseQuery.test.tsx", "src/react/hooks/__tests__/useBackgroundQuery.test.tsx", "src/react/hooks/__tests__/useLoadableQuery.test.tsx", diff --git a/docs/README.md b/docs/README.md index cb9d3ba95d3..9410556e966 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,4 +6,13 @@ The **deployed** version of the documentation for this repository is available a * https://www.apollographql.com/docs/react/ -See the [docs site README](https://github.com/apollographql/docs) for local installation and development. +For general local installation and development instructions, see the [docs site README](https://github.com/apollographql/docs). + +In order to build and develop the Apollo Client docs locally, you will need to follow these additional steps: + +1. Clone the docs site repository ([https://github.com/apollographql/docs](https://github.com/apollographql/docs)) in a sibling directory to your local copy of the [`apollo-client`](https://github.com/apollographql/apollo-client) repository. +2. `cd docs && npm i` +3. Open a new terminal, `cd apollo-client`, make changes to the docs and run `npm run docmodel` +4. Back in the terminal window where you've checked out and cd'd into the `docs` repository, run `DOCS_MODE='local' npm run start:local -- ../apollo-client` +5. Open a browser and visit `http://localhost:3000` +6. Note: you'll need to manually remove the `/react` segment of the URL from the path inside the browser diff --git a/docs/source/api/cache/InMemoryCache.mdx b/docs/source/api/cache/InMemoryCache.mdx index 7bcc362d9ea..4891eef7b01 100644 --- a/docs/source/api/cache/InMemoryCache.mdx +++ b/docs/source/api/cache/InMemoryCache.mdx @@ -2,8 +2,12 @@ title: class InMemoryCache description: API reference api_reference: true +api_doc: + - "@apollo/client!ApolloClient#watchFragment:member(1)" --- +import { FunctionDetails, DocBlock, Example } from '../../../shared/ApiDoc'; + Methods of the `InMemoryCache` class (the cache used by almost every instance of [`ApolloClient`](../core/ApolloClient/)) are documented here. > Before reading about individual methods, see [Caching in Apollo Client](../../caching/overview/). @@ -768,6 +772,9 @@ writeFragment( options: Cache.WriteFragmentOptions, ): Reference | undefined ``` + + + ## `updateFragment` diff --git a/docs/source/api/core/ApolloClient.mdx b/docs/source/api/core/ApolloClient.mdx index 752f6633d9e..0294973a18e 100644 --- a/docs/source/api/core/ApolloClient.mdx +++ b/docs/source/api/core/ApolloClient.mdx @@ -36,6 +36,7 @@ For more information on the `defaultOptions` object, see the [Default Options](# + diff --git a/docs/source/api/core/ObservableQuery.mdx b/docs/source/api/core/ObservableQuery.mdx index 806091f8b28..89c5448be7e 100644 --- a/docs/source/api/core/ObservableQuery.mdx +++ b/docs/source/api/core/ObservableQuery.mdx @@ -1,6 +1,7 @@ --- title: ObservableQuery description: API reference +api_reference: true api_doc: - "@apollo/client!ObservableQuery:class" - "@apollo/client!ApolloQueryResult:interface" diff --git a/docs/source/api/react/hooks-experimental.mdx b/docs/source/api/react/hooks-experimental.mdx index 99212fa7c30..2557f4e89a9 100644 --- a/docs/source/api/react/hooks-experimental.mdx +++ b/docs/source/api/react/hooks-experimental.mdx @@ -3,4 +3,4 @@ title: Hooks (experimental) description: Apollo Client experimental react hooks API reference --- -The latest minor version of Apollo Client (`3.8`) has no experimental hooks. Please see the [Hooks page](./hooks) for a list of available stable React hooks. +The latest minor version of Apollo Client has no experimental hooks. Please see the [Hooks page](./hooks) for a list of available stable React hooks. diff --git a/docs/source/caching/cache-interaction.mdx b/docs/source/caching/cache-interaction.mdx index 33baca6a11a..4331a5da14f 100644 --- a/docs/source/caching/cache-interaction.mdx +++ b/docs/source/caching/cache-interaction.mdx @@ -1,7 +1,11 @@ --- title: Reading and writing data to the cache +api_doc: + - "@apollo/client!ApolloClient#watchFragment:member(1)" --- +import { DocBlock } from '../../shared/ApiDoc'; + You can read and write data directly to the Apollo Client cache, _without_ communicating with your GraphQL server. You can interact with data that you previously fetched from your server, _and_ with data that's only available [locally](../local-state/local-state-management/). Apollo Client supports multiple strategies for interacting with cached data: @@ -211,6 +215,14 @@ client.writeFragment({ All subscribers to the Apollo Client cache (including all active queries) see this change and update your application's UI accordingly. + + +### `watchFragment` + + + + + ### `useFragment` diff --git a/docs/source/config.json b/docs/source/config.json index 98c46b99f90..5e85cecba5f 100644 --- a/docs/source/config.json +++ b/docs/source/config.json @@ -49,6 +49,7 @@ "Developer tools": "/development-testing/developer-tooling", "Using TypeScript": "/development-testing/static-typing", "Testing React components": "/development-testing/testing", + "Schema-driven testing": "/development-testing/schema-driven-testing", "Mocking schema capabilities": "/development-testing/client-schema-mocking", "Reducing bundle size": "/development-testing/reducing-bundle-size" }, diff --git a/docs/source/data/suspense.mdx b/docs/source/data/suspense.mdx index 5d2ad013344..3f5e0d0172e 100644 --- a/docs/source/data/suspense.mdx +++ b/docs/source/data/suspense.mdx @@ -535,12 +535,6 @@ We begin fetching our `GET_DOG_QUERY` by calling the `loadDog` function inside o - - -This feature is in [alpha](https://www.apollographql.com/docs/resources/product-launch-stages/#alpha--beta) in version `3.9.0` and is subject to change before `3.10.0`. We consider this feature production-ready, but it may change depending on feedback. If you'd like to provide feedback before it is stabilized in `3.10.0`, please comment on [#11519](https://github.com/apollographql/apollo-client/issues/11519). - - - Starting with Apollo Client `3.9.0`, queries can be initiated outside of React. This allows your app to begin fetching data before React renders your components, and can provide performance benefits. To preload queries, you first need to create a preload function with `createQueryPreloader`. `createQueryPreloader` takes an `ApolloClient` instance as an argument and returns a function that, when called, initiates a network request. @@ -677,12 +671,6 @@ export function RouteComponent() { This instructs React Router to wait for the query to finish loading before the route transitions. When the route transitions after the promise resolves, the data is rendered immediately without the need to show a loading fallback in the route component. - - -`queryRef.toPromise` is [experimental](https://www.apollographql.com/docs/resources/product-launch-stages/#experimental-features) in version `3.9.0` and is subject to breaking changes before `3.10.0`. If you'd like to provide feedback for this feature before it is stabilized in `3.10.0`, please comment on [#11519](https://github.com/apollographql/apollo-client/issues/11519). - - - #### Why prevent access to `data` in `toPromise`? You may be wondering why we resolve `toPromise` with the `queryRef` itself, rather than the data loaded from the query. We want to encourage you to leverage `useReadQuery` to avoid missing out on cache updates for your query. If `data` were available, it would be tempting to consume it in your `loader` functions and expose it to your route components. Doing so means missing out on cache updates. diff --git a/docs/source/development-testing/schema-driven-testing.mdx b/docs/source/development-testing/schema-driven-testing.mdx new file mode 100644 index 00000000000..3b477829b17 --- /dev/null +++ b/docs/source/development-testing/schema-driven-testing.mdx @@ -0,0 +1,560 @@ +--- +title: Schema-driven testing +description: Using createTestSchema and associated APIs +minVersion: 3.10.0 +--- + +This article describes best practices for writing integration tests using testing utilities released as experimental in v3.10. These testing tools allow developers to execute queries against a schema configured with mock resolvers and default scalar values in order to test an entire Apollo Client application, including the [link chain](/react/api/link/introduction). + +## Guiding principles + +Kent C. Dodds [said it best](https://twitter.com/kentcdodds/status/977018512689455106): + +> The more your tests resemble the way your software is used, the more confidence they can give you. + +When it comes to testing applications built with Apollo Client, this means validating the code path your users' requests will travel from the UI to the network layer and back. + +Unit-style testing with [`MockedProvider`](/react/development-testing/testing) can be useful for testing individual components—or even entire pages or React subtrees—in isolation by mocking the expected response data for individual operations. However, it's important to also test the integration of your components with the network layer. That's where schema-driven testing comes in. + +> This page is heavily inspired by the excellent [Redux documentation](https://redux.js.org/usage/writing-tests#guiding-principles); the same principles apply to Apollo Client. + +## `createTestSchema` and `createSchemaFetch` + +### Installation + +First, ensure you have installed Apollo Client v3.10 or greater. Then, install the following peer dependencies: + +```bash +npm i @graphql-tools/merge @graphql-tools/schema @graphql-tools/utils undici --save-dev +``` + +Consider a React application that fetches a list of products from a GraphQL server: + + + +```tsx title="products.tsx" +import { gql, TypedDocumentNode, useSuspenseQuery } from "@apollo/client"; + +type ProductsQuery = { + products: Array<{ + __typename: "Product"; + id: string; + title: string; + mediaUrl: string; + }>; +}; + +const PRODUCTS_QUERY: TypedDocumentNode = gql` + query ProductsQuery { + products { + id + title + mediaUrl + } + } +`; + +export function Products() { + const { data } = useSuspenseQuery(PRODUCTS_QUERY); + + return ( +
+ {data.products.map((product) => ( +

+ + {product.title} - {product.id} + +

+ ))} +
+ ); +} +``` + +
+ +Now let's write some tests using a test schema created with the `createTestSchema` utility that can then be used to create a mock fetch implementation with `createSchemaFetch`. + +### Configuring your test environment + +First, some Node.js globals will need to be polyfilled in order for JSDOM tests to run correctly. Create a file called e.g. `jest.polyfills.js`: + +```js title="jest.polyfills.js" +/** + * @note The block below contains polyfills for Node.js globals + * required for Jest to function when running JSDOM tests. + * These have to be require's and have to be in this exact + * order, since "undici" depends on the "TextEncoder" global API. + */ + +const { TextDecoder, TextEncoder } = require("node:util"); +const { ReadableStream } = require("node:stream/web"); +const { clearImmediate } = require("node:timers"); +const { performance } = require("node:perf_hooks"); + +Object.defineProperties(globalThis, { + TextDecoder: { value: TextDecoder }, + TextEncoder: { value: TextEncoder }, + ReadableStream: { value: ReadableStream }, + performance: { value: performance }, + clearImmediate: { value: clearImmediate }, +}); + +const { Blob, File } = require("node:buffer"); +const { fetch, Headers, FormData, Request, Response } = require("undici"); + +Object.defineProperties(globalThis, { + fetch: { value: fetch, writable: true }, + Response: { value: Response }, + Blob: { value: Blob }, + File: { value: File }, + Headers: { value: Headers }, + FormData: { value: FormData }, + Request: { value: Request }, +}); + +// Note: if your environment supports it, you can use the `using` keyword +// but must polyfill Symbol.dispose here with Jest versions <= 29 +// where Symbol.dispose is not defined +// +// Jest bug: https://github.com/jestjs/jest/issues/14874 +// Fix is available in https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.3 +if (!Symbol.dispose) { + Object.defineProperty(Symbol, "dispose", { + value: Symbol("dispose"), + }); +} +if (!Symbol.asyncDispose) { + Object.defineProperty(Symbol, "asyncDispose", { + value: Symbol("asyncDispose"), + }); +} +``` + +Now, in a `jest.config.ts` or `jest.config.js` file, add the following configuration: + +```ts title="jest.config.ts" +import type { Config } from "jest"; + +const config: Config = { + globals: { + "globalThis.__DEV__": JSON.stringify(true), + }, + testEnvironment: "jsdom", + setupFiles: ["./jest.polyfills.js"], + // You may also have an e.g. setupTests.ts file here + setupFilesAfterEnv: ["/setupTests.ts"], + // If you're using MSW, opt out of the browser export condition for MSW tests + // For more information, see: https://github.com/mswjs/msw/issues/1786#issuecomment-1782559851 + testEnvironmentOptions: { + customExportConditions: [""], + }, + // If you plan on importing .gql/.graphql files in your tests, transform them with @graphql-tools/jest-transform + transform: { + "\\.(gql|graphql)$": "@graphql-tools/jest-transform", + }, +}; + +export default config; +``` + +In the example `setupTests.ts` file below, `@testing-library/jest-dom` is imported to allow the use of custom `jest-dom` matchers (see the [`@testing-library/jest-dom` documentation](https://github.com/testing-library/jest-dom?tab=readme-ov-file#usage) for more information) and fragment warnings are disabled which can pollute the test output: + +```ts title="setupTests.ts" +import "@testing-library/jest-dom"; +import { gql } from "@apollo/client"; + +gql.disableFragmentWarnings(); +``` + +### Testing with MSW + +Now, let's write a test for the `Products` component using [MSW](https://mswjs.io/). + +MSW is a powerful tool for intercepting network traffic and mocking responses. Read more about its design and philosophy [here](https://mswjs.io/blog/why-mock-service-worker/). + +MSW has the concept of [handlers](https://mswjs.io/docs/best-practices/structuring-handlers/) that allow network requests to be intercepted. Let's create a handler that will intercept all GraphQL operations: + +```ts title="src/__tests__/handlers.ts" +import { graphql, HttpResponse } from "msw"; +import { execute } from "graphql"; +import type { ExecutionResult } from "graphql"; +import type { ObjMap } from "graphql/jsutils/ObjMap"; +import { gql } from "@apollo/client"; +import { createTestSchema } from "@apollo/client/testing/experimental"; +import { makeExecutableSchema } from "@graphql-tools/schema"; +import graphqlSchema from "../../../schema.graphql"; + +// First, create a static schema... +const staticSchema = makeExecutableSchema({ typeDefs: graphqlSchema }); + +// ...which is then passed as the first argument to `createTestSchema` +// along with mock resolvers and default scalar values. +export let testSchema = createTestSchema(staticSchema, { + resolvers: { + Query: { + products: () => [ + { + id: "1", + title: "Blue Jays Hat", + }, + ], + }, + }, + scalars: { + Int: () => 6, + Float: () => 22.1, + String: () => "string", + }, +}); + +export const handlers = [ + // Intercept all GraphQL operations and return a response generated by the + // test schema. Add additional handlers as needed. + graphql.operation, ObjMap>>( + async ({ query, variables, operationName }) => { + const document = gql(query); + + const result = await execute({ + document, + operationName, + schema: testSchema, + variableValues: variables, + }); + + return HttpResponse.json(result); + } + ), +]; +``` + +MSW can be used in [the browser](https://mswjs.io/docs/integrations/browser), in [Node.js](https://mswjs.io/docs/integrations/node) and in [React Native](https://mswjs.io/docs/integrations/react-native). Since this example is using Jest and JSDOM to run tests in a Node.js environment, let's configure the server per the [Node.js integration guide](https://mswjs.io/docs/integrations/node): + +```ts title="src/__tests__/server.ts" +import { setupServer } from "msw/node"; +import { handlers } from "./handlers"; + +// This configures a request mocking server with the given request handlers. +export const server = setupServer(...handlers); +``` + +Finally, let's do server set up and teardown in the `setupTests.ts` file created in the previous step: + +```ts title="setupTests.ts" {6-8} +import "@testing-library/jest-dom"; +import { gql } from "@apollo/client"; + +gql.disableFragmentWarnings(); + +beforeAll(() => server.listen({ onUnhandledRequest: "error" })); +afterAll(() => server.close()); +afterEach(() => server.resetHandlers()); +``` + +Finally, let's write some tests 🎉 + +```tsx title="src/__tests__/products.test.tsx" +import { Suspense } from "react"; +import { render as rtlRender, screen } from "@testing-library/react"; +import { + ApolloClient, + ApolloProvider, + NormalizedCacheObject, +} from "@apollo/client"; +import { testSchema } from "./handlers"; +import { Products } from "../products"; +// This should be a function that returns a new ApolloClient instance +// configured just like your production Apollo Client instance - see the FAQ. +import { makeClient } from "../client"; + +const render = (renderedClient: ApolloClient) => + rtlRender( + + + + + + ); + +describe("Products", () => { + test("renders", async () => { + render(makeClient()); + + await screen.findByText("Loading..."); + + // This is the data from our initial mock resolver in the test schema + // defined in the handlers file 🎉 + expect(await screen.findByText(/blue jays hat/i)).toBeInTheDocument(); + }); + + test("allows resolvers to be updated via .add", async () => { + // Calling .add on the test schema will update the resolvers + // with new data + testSchema.add({ + resolvers: { + Query: { + products: () => { + return [ + { + id: "2", + title: "Mets Hat", + }, + ]; + }, + }, + }, + }); + + render(makeClient()); + + await screen.findByText("Loading..."); + + // Our component now renders the new data from the updated resolver + await screen.findByText(/mets hat/i); + }); + + test("handles test schema resetting via .reset", async () => { + // Calling .reset on the test schema will reset the resolvers + testSchema.reset(); + + render(makeClient()); + + await screen.findByText("Loading..."); + + // The component now renders the initial data configured on the test schema + await screen.findByText(/blue jays hat/i); + }); +}); +``` + +### Testing by mocking fetch with `createSchemaFetch` + +First, import `createSchemaFetch` and `createTestSchema` from the new `@apollo/client/testing` entrypoint. Next, import a local copy of your graph's schema: jest should be configured to transform `.gql` or `.graphql` files using `@graphql-tools/jest-transform` (see the `jest.config.ts` example configuration above.) + +Here's how an initial set up of the test file might look: + +```tsx title="products.test.tsx" +import { + createSchemaFetch, + createTestSchema, +} from "@apollo/client/testing/experimental"; +import { makeExecutableSchema } from "@graphql-tools/schema"; +import { render as rtlRender, screen } from "@testing-library/react"; +import graphqlSchema from "../../../schema.graphql"; +// This should be a function that returns a new ApolloClient instance +// configured just like your production Apollo Client instance - see the FAQ. +import { makeClient } from "../../client"; +import { ApolloProvider, NormalizedCacheObject } from "@apollo/client"; +import { Products } from "../../products"; +import { Suspense } from "react"; + +// First, let's create an executable schema... +const staticSchema = makeExecutableSchema({ typeDefs: graphqlSchema }); + +// which is then passed as the first argument to `createTestSchema`. +const schema = createTestSchema(staticSchema, { + // Next, let's define mock resolvers + resolvers: { + Query: { + products: () => + Array.from({ length: 5 }, (_element, id) => ({ + id, + mediaUrl: `https://example.com/image${id}.jpg`, + })), + }, + }, + // ...and default scalar values + scalars: { + Int: () => 6, + Float: () => 22.1, + String: () => "default string", + }, +}); + +// This `render` helper function would typically be extracted and shared between +// test files. +const render = (renderedClient: ApolloClient) => + rtlRender( + + + + + + ); +``` + +Now let's write some tests 🎉 + +First, `createSchemaFetch` can be used to mock the global `fetch` implementation with one that resolves network requests with payloads generated from the test schema. + +```tsx title="products.test.tsx" +describe("Products", () => { + it("renders", async () => { + using _fetch = createSchemaFetch(schema).mockGlobal(); + + render(makeClient()); + + await screen.findByText("Loading..."); + + // title is rendering the default string scalar + const findAllByText = await screen.findAllByText(/default string/); + expect(findAllByText).toHaveLength(5); + + // the products resolver is returning 5 products + await screen.findByText(/0/); + await screen.findByText(/1/); + await screen.findByText(/2/); + await screen.findByText(/3/); + await screen.findByText(/4/); + }); +}); +``` + +#### A note on `using` and explicit resource management + +You may have noticed a new keyword in the first line of the test above: `using`. + +`using` is part of a [proposed new language feature](https://github.com/tc39/proposal-explicit-resource-management) which is currently at Stage 3 of the TC39 proposal process. + +If you are using TypeScript 5.2 or greater, or using Babel's [`@babel/plugin-proposal-explicit-resource-management` plugin](https://babeljs.io/docs/babel-plugin-proposal-explicit-resource-management), you can use the `using` keyword to automatically perform some cleanup when `_fetch` goes out of scope. In our case, this is when the test is complete; this means restoring the global fetch function to its original state automatically after each test. + +If your environment does not support explicit resource management, you'll find that calling `mockGlobal()` returns a restore function that you can manually call at the end of each test: + +```tsx title="products.test.tsx" +describe("Products", () => { + it("renders", async () => { + const { restore } = createSchemaFetch(schema).mockGlobal(); + + render(makeClient()); + + // make assertions against the rendered DOM output + + restore(); + }); +}); +``` + +## Modifying a test schema using `testSchema.add` and `testSchema.fork` + +If you need to make changes to the behavior of a schema after it has been created, you can use the `testSchema.add` method to add new resolvers to the schema or overwrite existing ones. +This can be useful for testing scenarios where the behavior of the schema needs to change inside a test. + +````tsx title="products.test.tsx" +describe("Products", () => { + it("renders", async () => { + const { restore } = createSchemaFetch(schema).mockGlobal(); + + render(makeClient()); + + // make assertions against the rendered DOM output + + // Here we want to change the return value of the `products` resolver + // for the next outgoing query. + testSchema.add({ + resolvers: { + Query: { + products: () => + Array.from({ length: 5 }, (_element, id) => ({ + // we want to return ids starting from 5 for the second request + id: id + 5, + mediaUrl: `https://example.com/image${id + 5}.jpg`, + })), + }, + }, + }); + + // trigger a new query with a user interaction + userEvent.click(screen.getByText("Fetch more")); + + // make assertions against the rendered DOM output + + restore(); + testSchema.reset(); + }); +}); +``` + +Alternatively, you can use `testSchema.fork` to create a new schema with the same configuration as the original schema, +but with the ability to make changes to the new isolated schema without affecting the original schema. +This can be useful if you just want to mock the global fetch function with a different schema for each test without +having to care about resetting your original testSchema. +You could also write incremental tests where each test builds on the previous one. + +If you use MSW, you will probably end up using `testSchema.add`, as MSW needs to be set up with a single schema for all tests. +If you are going the `createSchemaFetch` route, you can use `testSchema.fork` to create a new schema for each test, +and then use `forkedSchema.add` to make changes to the schema for that test. + +```tsx +const baseSchema = createTestSchema(staticSchema, { + resolvers: { + // ... + }, + scalars: { + // ... + }, +}); + +test("a test", () => { + const forkedSchema = baseSchema.fork(); + + const { restore } = createSchemaFetch(forkedSchema).mockGlobal(); + + // make assertions against the rendered DOM output + + forkedSchema.add({ + // ... + }); + + restore(); + // forkedSchema will just be discarded, and there is no need to reset it +}); +```` + +### FAQ + +#### When should I use `createSchemaFetch` vs [MSW](https://mswjs.io/)? + +There are many benefits to using [MSW](https://mswjs.io/): it's a powerful tool with a great set of APIs. Read more about its philosophy and benefits [here](https://mswjs.io/blog/why-mock-service-worker/). + +Wherever possible, use MSW: it enables more realistic tests that can catch more bugs by intercepting requests _after_ they've been dispatched by an application. MSW also supports both REST and GraphQL handlers, so if your application uses a combination (e.g. to fetch data from a third party endpoint), MSW will provide more flexibility than `createSchemaFetch`, which is a more lightweight solution. + +#### Should I share a single `ApolloClient` instance between tests? + +No; please create a new instance of `ApolloClient` for each test. Even if the cache is reset in between tests, the client maintains some internal state that is not reset. This could have some unintended consequences. For example, the `ApolloClient` instance could have pending queries that could cause the following test's queries to be deduplicated by default. + +Instead, create a `makeClient` function or equivalent so that every test uses the same client configuration as your production client, but no two tests share the same client instance. Here's an example: + + + +```ts title="src/client.ts" +import { ApolloClient, HttpLink, InMemoryCache } from "@apollo/client"; + +const httpLink = new HttpLink({ + uri: "https://example.com/graphql", +}); + +export const makeClient = () => { + return new ApolloClient({ + cache: new InMemoryCache(), + link: httpLink, + }); +}; + +export const client = makeClient(); +``` + + + +This way, every test can use `makeClient` to create a new client instance, and you can still use `client` in your production code. + +#### Can I use these testing tools with Vitest? + +Unfortunately not at the moment. This is caused by a known limitation with the `graphql` package and tools that bundle ESM by default known as the [dual package hazard](https://nodejs.org/api/packages.html#dual-package-hazard). + +Please see [this issue](https://github.com/graphql/graphql-js/issues/4062) to track the related discussion on the `graphql/graphql-js` repository. + +## Sandbox example + +For a working example that demonstrates how to use both Testing Library and Mock Service Worker to write integration tests with `createTestSchema`, check out this project on CodeSandbox: + +[![Edit Testing React Components](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/apollographql/docs-examples/tree/main/apollo-client/v3/testing-react-components?file=/src/dog.test.js) diff --git a/docs/source/development-testing/static-typing.md b/docs/source/development-testing/static-typing.md index 3d11003c0c3..96a5148550c 100644 --- a/docs/source/development-testing/static-typing.md +++ b/docs/source/development-testing/static-typing.md @@ -16,7 +16,7 @@ Below, we'll guide you through installing and configuring GraphQL Code Generator To get started using GraphQL Code Generator, begin by installing the following packages (using Yarn or NPM): ```bash -yarn add -D typescript @graphql-codegen/cli @graphql-codegen/client-preset @graphql-typed-document-node/core +yarn add -D typescript graphql @graphql-codegen/cli @graphql-codegen/client-preset @graphql-typed-document-node/core ``` Next, we'll create a configuration file for GraphQL Code Generator, named [`codegen.ts`](https://www.the-guild.dev/graphql/codegen/docs/config-reference/codegen-config), at the root of our project: diff --git a/package-lock.json b/package-lock.json index 71841a502a6..0d79b181395 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.9.11", + "version": "3.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.9.11", + "version": "3.10.0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -18,7 +18,7 @@ "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", - "rehackt": "0.0.6", + "rehackt": "^0.1.0", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", @@ -30,7 +30,9 @@ "@babel/parser": "7.24.1", "@changesets/changelog-github": "0.5.0", "@changesets/cli": "2.27.1", + "@graphql-tools/merge": "^9.0.3", "@graphql-tools/schema": "10.0.3", + "@graphql-tools/utils": "10.0.13", "@microsoft/api-extractor": "7.42.3", "@rollup/plugin-node-resolve": "11.2.1", "@size-limit/esbuild-why": "11.1.2", @@ -1861,9 +1863,9 @@ } }, "node_modules/@graphql-tools/utils": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.1.0.tgz", - "integrity": "sha512-wLPqhgeZ9BZJPRoaQbsDN/CtJDPd/L4qmmtPkjI3NuYJ39x+Eqz1Sh34EAGMuDh+xlOHqBwHczkZUpoK9tvzjw==", + "version": "10.0.13", + "resolved": "https://registry.npmjs.org/@graphql-tools/utils/-/utils-10.0.13.tgz", + "integrity": "sha512-fMILwGr5Dm2zefNItjQ6C2rauigklv69LIwppccICuGTnGaOp3DspLt/6Lxj72cbg5d9z60Sr+Egco3CJKLsNg==", "dev": true, "dependencies": { "@graphql-typed-document-node/core": "^3.1.1", @@ -10689,9 +10691,9 @@ } }, "node_modules/rehackt": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.0.6.tgz", - "integrity": "sha512-l3WEzkt4ntlEc/IB3/mF6SRgNHA6zfQR7BlGOgBTOmx7IJJXojDASav+NsgXHFjHn+6RmwqsGPFgZpabWpeOdw==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/rehackt/-/rehackt-0.1.0.tgz", + "integrity": "sha512-7kRDOuLHB87D/JESKxQoRwv4DzbIdwkAGQ7p6QKGdVlY1IZheUnVhlk/4UZlNUVxdAXpyxikE3URsG067ybVzw==", "peerDependencies": { "@types/react": "*", "react": "*" diff --git a/package.json b/package.json index f43da69fd82..a4157dcc021 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.9.11", + "version": "3.10.0", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ @@ -99,7 +99,7 @@ "hoist-non-react-statics": "^3.3.2", "optimism": "^0.18.0", "prop-types": "^15.7.2", - "rehackt": "0.0.6", + "rehackt": "^0.1.0", "response-iterator": "^0.2.6", "symbol-observable": "^4.0.0", "ts-invariant": "^0.10.3", @@ -111,7 +111,9 @@ "@babel/parser": "7.24.1", "@changesets/changelog-github": "0.5.0", "@changesets/cli": "2.27.1", + "@graphql-tools/merge": "^9.0.3", "@graphql-tools/schema": "10.0.3", + "@graphql-tools/utils": "10.0.13", "@microsoft/api-extractor": "7.42.3", "@rollup/plugin-node-resolve": "11.2.1", "@size-limit/esbuild-why": "11.1.2", diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 993691abe0c..67ce0002714 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -14,7 +14,7 @@ import { ApolloLink } from "../link/core"; import { HttpLink } from "../link/http"; import { InMemoryCache } from "../cache"; import { itAsync } from "../testing"; -import { spyOnConsole } from "../testing/internal"; +import { ObservableStream, spyOnConsole } from "../testing/internal"; import { TypedDocumentNode } from "@graphql-typed-document-node/core"; import { invariant } from "../utilities/globals"; @@ -2174,6 +2174,263 @@ describe("ApolloClient", () => { ); }); + describe("watchFragment", () => { + it("if all data is available, `complete` is `true`", async () => { + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + }); + const ItemFragment = gql` + fragment ItemFragment on Item { + id + text + } + `; + + cache.writeFragment({ + fragment: ItemFragment, + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + }); + + const observable = client.watchFragment({ + fragment: ItemFragment, + from: { __typename: "Item", id: 5 }, + }); + + const stream = new ObservableStream(observable); + + { + const result = await stream.takeNext(); + + expect(result).toEqual({ + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + complete: true, + }); + } + }); + it("cache writes emit a new value", async () => { + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + }); + const ItemFragment = gql` + fragment ItemFragment on Item { + id + text + } + `; + + cache.writeFragment({ + fragment: ItemFragment, + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + }); + + const observable = client.watchFragment({ + fragment: ItemFragment, + from: { __typename: "Item", id: 5 }, + }); + + const stream = new ObservableStream(observable); + + { + const result = await stream.takeNext(); + + expect(result).toEqual({ + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + complete: true, + }); + } + + cache.writeFragment({ + fragment: ItemFragment, + data: { + __typename: "Item", + id: 5, + text: "Item #5 (edited)", + }, + }); + + { + const result = await stream.takeNext(); + + expect(result).toEqual({ + data: { + __typename: "Item", + id: 5, + text: "Item #5 (edited)", + }, + complete: true, + }); + } + }); + it("if only partial data is available, `complete` is `false`", async () => { + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + }); + const ItemFragment = gql` + fragment ItemFragment on Item { + id + text + } + `; + + { + // we expect a "Missing field 'text' while writing result..." error + // when writing item to the cache, so we'll silence the console.error + using _consoleSpy = spyOnConsole("error"); + cache.writeFragment({ + fragment: ItemFragment, + data: { + __typename: "Item", + id: 5, + }, + }); + } + + const observable = client.watchFragment({ + fragment: ItemFragment, + from: { __typename: "Item", id: 5 }, + }); + + const stream = new ObservableStream(observable); + + { + const result = await stream.takeNext(); + + expect(result).toEqual({ + data: { + __typename: "Item", + id: 5, + }, + complete: false, + missing: { + text: "Can't find field 'text' on Item:5 object", + }, + }); + } + }); + it("if no data is written after observable is subscribed to, next is never called", async () => { + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + }); + const ItemFragment = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const observable = client.watchFragment({ + fragment: ItemFragment, + from: { __typename: "Item", id: 5 }, + }); + + const stream = new ObservableStream(observable); + + { + const result = await stream.takeNext(); + + expect(result).toEqual({ + complete: false, + data: {}, + missing: "Dangling reference to missing Item:5 object", + }); + } + + await expect(stream.takeNext({ timeout: 1000 })).rejects.toEqual( + expect.any(Error) + ); + }); + // The @nonreactive directive can only be used on fields or fragment + // spreads in queries, and currently has no effect here + it.failing("does not support the @nonreactive directive", async () => { + const cache = new InMemoryCache(); + const client = new ApolloClient({ + cache, + link: ApolloLink.empty(), + }); + const ItemFragment = gql` + fragment ItemFragment on Item { + id + text @nonreactive + } + `; + + cache.writeFragment({ + fragment: ItemFragment, + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + }); + + const observable = client.watchFragment({ + fragment: ItemFragment, + from: { __typename: "Item", id: 5 }, + }); + + const stream = new ObservableStream(observable); + + { + const result = await stream.takeNext(); + + expect(result).toEqual({ + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + complete: true, + }); + } + + cache.writeFragment({ + fragment: ItemFragment, + data: { + __typename: "Item", + id: 5, + text: "Item #5 (edited)", + }, + }); + + { + const result = await stream.takeNext(); + + expect(result).toEqual({ + data: { + __typename: "Item", + id: 5, + text: "Item #5", + }, + complete: true, + }); + } + }); + }); + describe("defaultOptions", () => { it( "should set `defaultOptions` to an empty object if not provided in " + diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index a2e89a93514..f5a1dfd86bc 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -398,6 +398,13 @@ Array [ ] `; +exports[`exports of public entry points @apollo/client/testing/experimental 1`] = ` +Array [ + "createSchemaFetch", + "createTestSchema", +] +`; + exports[`exports of public entry points @apollo/client/utilities 1`] = ` Array [ "AutoCleanedStrongCache", diff --git a/src/__tests__/exports.ts b/src/__tests__/exports.ts index 181a1cc2b0a..d50b933810c 100644 --- a/src/__tests__/exports.ts +++ b/src/__tests__/exports.ts @@ -31,6 +31,7 @@ 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 testingExperimental from "../testing/experimental"; import * as utilities from "../utilities"; import * as utilitiesGlobals from "../utilities/globals"; import * as urqlUtilities from "../utilities/subscriptions/urql"; @@ -77,6 +78,7 @@ describe("exports of public entry points", () => { check("@apollo/client/react/ssr", reactSSR); check("@apollo/client/testing", testing); check("@apollo/client/testing/core", testingCore); + check("@apollo/client/testing/experimental", testingExperimental); check("@apollo/client/utilities", utilities); check("@apollo/client/utilities/globals", utilitiesGlobals); check("@apollo/client/utilities/subscriptions/urql", urqlUtilities); diff --git a/src/cache/core/cache.ts b/src/cache/core/cache.ts index a0fd1778bdc..6c9e4108223 100644 --- a/src/cache/core/cache.ts +++ b/src/cache/core/cache.ts @@ -1,19 +1,102 @@ import type { DocumentNode } from "graphql"; import { wrap } from "optimism"; -import type { StoreObject, Reference } from "../../utilities/index.js"; +import type { + StoreObject, + Reference, + DeepPartial, +} from "../../utilities/index.js"; import { + Observable, cacheSizes, defaultCacheSizes, getFragmentQueryDocument, + mergeDeepArray, } 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"; +import type { + OperationVariables, + TypedDocumentNode, +} from "../../core/types.js"; +import { equal } from "@wry/equality"; +import type { MissingTree } from "./types/common.js"; export type Transaction = (c: ApolloCache) => void; +/** + * Watched fragment options. + */ +export interface WatchFragmentOptions { + /** + * A GraphQL fragment document parsed into an AST with the `gql` + * template literal. + * + * @docGroup 1. Required options + */ + fragment: DocumentNode | TypedDocumentNode; + /** + * An object containing a `__typename` and primary key fields + * (such as `id`) identifying the entity object from which the fragment will + * be retrieved, or a `{ __ref: "..." }` reference, or a `string` ID + * (uncommon). + * + * @docGroup 1. Required options + */ + from: StoreObject | Reference | string; + /** + * Any variables that the GraphQL fragment may depend on. + * + * @docGroup 2. Cache options + */ + variables?: TVars; + /** + * The name of the fragment defined in the fragment document. + * + * Required if the fragment document includes more than one fragment, + * optional otherwise. + * + * @docGroup 2. Cache options + */ + fragmentName?: string; + /** + * If `true`, `watchFragment` returns optimistic results. + * + * The default value is `true`. + * + * @docGroup 2. Cache options + */ + optimistic?: 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. + * + * Whether to canonize cache results before returning them. Canonization + * takes some extra time, but it speeds up future deep equality comparisons. + * Defaults to false. + */ + canonizeResults?: boolean; +} + +/** + * Watched fragment results. + */ +export type WatchFragmentResult = + | { + data: TData; + complete: true; + missing?: never; + } + | { + data: DeepPartial; + complete: false; + missing: MissingTree; + }; + export abstract class ApolloCache implements DataProxy { public readonly assumeImmutableResults: boolean = false; @@ -141,6 +224,49 @@ export abstract class ApolloCache implements DataProxy { }); } + /** {@inheritDoc @apollo/client!ApolloClient#watchFragment:member(1)} */ + public watchFragment( + options: WatchFragmentOptions + ): Observable> { + const { fragment, fragmentName, from, optimistic = true } = options; + + const diffOptions: Cache.DiffOptions = { + returnPartialData: true, + id: typeof from === "string" ? from : this.identify(from), + query: this.getFragmentDoc(fragment, fragmentName), + optimistic, + }; + + let latestDiff: DataProxy.DiffResult | undefined; + + return new Observable((observer) => { + return this.watch({ + ...diffOptions, + immediate: true, + query: this.getFragmentDoc(fragment, fragmentName), + callback(diff) { + if (equal(diff, latestDiff)) { + return; + } + + const result = { + data: diff.result as DeepPartial, + complete: !!diff.complete, + } as WatchFragmentResult; + + if (diff.missing) { + result.missing = mergeDeepArray( + diff.missing.map((error) => error.missing) + ); + } + + latestDiff = diff; + observer.next(result); + }, + }); + }); + } + // Make sure we compute the same (===) fragment query document every // time we receive the same fragment in readFragment. private getFragmentDoc = wrap(getFragmentQueryDocument, { diff --git a/src/cache/index.ts b/src/cache/index.ts index d57341ff2ac..1f55d8c16df 100644 --- a/src/cache/index.ts +++ b/src/cache/index.ts @@ -1,6 +1,10 @@ import "../utilities/globals/index.js"; -export type { Transaction } from "./core/cache.js"; +export type { + Transaction, + WatchFragmentOptions, + WatchFragmentResult, +} from "./core/cache.js"; export { ApolloCache } from "./core/cache.js"; export { Cache } from "./core/types/Cache.js"; export type { DataProxy } from "./core/types/DataProxy.js"; diff --git a/src/core/ApolloClient.ts b/src/core/ApolloClient.ts index 2f0b991872e..42daf602226 100644 --- a/src/core/ApolloClient.ts +++ b/src/core/ApolloClient.ts @@ -128,6 +128,10 @@ export interface ApolloClientOptions { // solution is to reexport mergeOptions where it was previously declared (here). import { mergeOptions } from "../utilities/index.js"; import { getApolloClientMemoryInternals } from "../utilities/caching/getMemoryInternals.js"; +import type { + WatchFragmentOptions, + WatchFragmentResult, +} from "../cache/core/cache.js"; export { mergeOptions }; /** @@ -235,6 +239,7 @@ export class ApolloClient implements DataProxy { this.watchQuery = this.watchQuery.bind(this); this.query = this.query.bind(this); this.mutate = this.mutate.bind(this); + this.watchFragment = this.watchFragment.bind(this); this.resetStore = this.resetStore.bind(this); this.reFetchObservableQueries = this.reFetchObservableQueries.bind(this); @@ -355,7 +360,7 @@ export class ApolloClient implements DataProxy { /** * This watches the cache store of the query according to the options specified and * returns an `ObservableQuery`. We can subscribe to this `ObservableQuery` and - * receive updated results through a GraphQL observer when the cache store changes. + * receive updated results through an observer when the cache store changes. * * Note that this method is not an implementation of GraphQL subscriptions. Rather, * it uses Apollo's store in order to reactively deliver updates to your query results. @@ -474,6 +479,32 @@ export class ApolloClient implements DataProxy { return this.cache.readQuery(options, optimistic); } + /** + * Watches the cache store of the fragment according to the options specified + * and returns an `Observable`. We can subscribe to this + * `Observable` and receive updated results through an + * observer when the cache store changes. + * + * You must pass in a GraphQL document with a single fragment or a document + * with multiple fragments that represent what you are reading. If you pass + * in a document with multiple fragments then you must also specify a + * `fragmentName`. + * + * @since 3.10.0 + * @param options - An object of type `WatchFragmentOptions` that allows + * the cache to identify the fragment and optionally specify whether to react + * to optimistic updates. + */ + + public watchFragment< + TFragmentData = unknown, + TVariables = OperationVariables, + >( + options: WatchFragmentOptions + ): Observable> { + return this.cache.watchFragment(options); + } + /** * Tries to read some data from the store in the shape of the provided * GraphQL fragment without making a network request. This method will read a diff --git a/src/core/index.ts b/src/core/index.ts index 5757cdb2071..785b2d03efc 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -40,6 +40,8 @@ export type { FieldMergeFunction, FieldFunctionOptions, PossibleTypesMap, + WatchFragmentOptions, + WatchFragmentResult, } from "../cache/index.js"; export { Cache, diff --git a/src/link/persisted-queries/__tests__/persisted-queries.test.ts b/src/link/persisted-queries/__tests__/persisted-queries.test.ts index 32d75fe5136..7b4fecaf99f 100644 --- a/src/link/persisted-queries/__tests__/persisted-queries.test.ts +++ b/src/link/persisted-queries/__tests__/persisted-queries.test.ts @@ -544,7 +544,8 @@ describe("failure path", () => { ); it.each([ - ["error message", giveUpResponse], + // TODO(fixme): test flake on CI https://github.com/apollographql/apollo-client/issues/11782 + // ["error message", giveUpResponse], ["error code", giveUpResponseWithCode], ] as const)( "clears the cache when receiving NotSupported error (%s)", @@ -573,7 +574,11 @@ describe("failure path", () => { variables, }).subscribe({ complete }) ); - + // fetch-mock holds a history of all options it has been called with + // that includes the `signal` option, which (with the native `AbortController`) + // has a reference to the `Request` instance, which will somehow reference our + // hash object + fetchMock.resetHistory(); await expect(hashRefs[0]).toBeGarbageCollected(); } ); diff --git a/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx b/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx index a8cf0f0a69a..0bf53ed8ab5 100644 --- a/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx +++ b/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx @@ -1,5 +1,5 @@ /* eslint-disable testing-library/render-result-naming-convention */ -import React, { useEffect } from "react"; +import React, { useEffect } from "rehackt"; import { useRenderGuard } from "../useRenderGuard"; import { render, waitFor } from "@testing-library/react"; import { withCleanup } from "../../../../testing/internal"; diff --git a/src/react/hooks/internal/wrapHook.ts b/src/react/hooks/internal/wrapHook.ts index abf9a49c035..c22ec726e9d 100644 --- a/src/react/hooks/internal/wrapHook.ts +++ b/src/react/hooks/internal/wrapHook.ts @@ -4,6 +4,7 @@ import type { useBackgroundQuery, useReadQuery, useFragment, + useQueryRefHandlers, } from "../index.js"; import type { QueryManager } from "../../../core/QueryManager.js"; import type { ApolloClient } from "../../../core/ApolloClient.js"; @@ -17,6 +18,7 @@ interface WrappableHooks { useBackgroundQuery: typeof useBackgroundQuery; useReadQuery: typeof useReadQuery; useFragment: typeof useFragment; + useQueryRefHandlers: typeof useQueryRefHandlers; } /** diff --git a/src/react/hooks/useFragment.ts b/src/react/hooks/useFragment.ts index 368e6273d85..6f94e8edaac 100644 --- a/src/react/hooks/useFragment.ts +++ b/src/react/hooks/useFragment.ts @@ -1,6 +1,4 @@ import * as React from "rehackt"; -import { equal } from "@wry/equality"; - import type { DeepPartial } from "../../utilities/index.js"; import { mergeDeepArray } from "../../utilities/index.js"; import type { @@ -15,6 +13,7 @@ import { useSyncExternalStore } from "./useSyncExternalStore.js"; import type { ApolloClient, OperationVariables } from "../../core/index.js"; import type { NoInfer } from "../types/types.js"; import { useDeepMemo, useLazyRef, wrapHook } from "./internal/index.js"; +import equal from "@wry/equality"; export interface UseFragmentOptions extends Omit< @@ -88,6 +87,8 @@ function _useFragment( diffToResult(cache.diff(diffOptions)) ); + const stableOptions = useDeepMemo(() => options, [options]); + // Since .next is async, we need to make sure that we // get the correct diff on the next render given new diffOptions React.useMemo(() => { @@ -101,27 +102,24 @@ function _useFragment( React.useCallback( (forceUpdate) => { let lastTimeout = 0; - const unsubscribe = cache.watch({ - ...diffOptions, - immediate: true, - callback(diff) { - if (!equal(diff.result, resultRef.current.data)) { - resultRef.current = diffToResult(diff); - // If we get another update before we've re-rendered, bail out of - // the update and try again. This ensures that the relative timing - // between useQuery and useFragment stays roughly the same as - // fixed in https://github.com/apollographql/apollo-client/pull/11083 - clearTimeout(lastTimeout); - lastTimeout = setTimeout(forceUpdate) as any; - } + const subscription = cache.watchFragment(stableOptions).subscribe({ + next: (result) => { + if (equal(result, resultRef.current)) return; + resultRef.current = result; + // If we get another update before we've re-rendered, bail out of + // the update and try again. This ensures that the relative timing + // between useQuery and useFragment stays roughly the same as + // fixed in https://github.com/apollographql/apollo-client/pull/11083 + clearTimeout(lastTimeout); + lastTimeout = setTimeout(forceUpdate) as any; }, }); return () => { - unsubscribe(); + subscription.unsubscribe(); clearTimeout(lastTimeout); }; }, - [cache, diffOptions] + [cache, stableOptions] ), getSnapshot, getSnapshot diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts index b0422afa678..0d6809e6ca6 100644 --- a/src/react/hooks/useQueryRefHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -5,10 +5,15 @@ import { updateWrappedQueryRef, wrapQueryRef, } from "../internal/index.js"; -import type { QueryReference } from "../internal/index.js"; +import type { + InternalQueryReference, + 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"; +import { useApolloClient } from "./useApolloClient.js"; +import { wrapHook } from "./internal/index.js"; export interface UseQueryRefHandlersResult< TData = unknown, @@ -44,6 +49,34 @@ export function useQueryRefHandlers< TVariables extends OperationVariables = OperationVariables, >( queryRef: QueryReference +): UseQueryRefHandlersResult { + const unwrapped = unwrapQueryRef( + queryRef + ) satisfies InternalQueryReference as /* + by all rules of this codebase, this should never be undefined + but if `queryRef` is a transported object, it cannot have a + `QUERY_REFERENCE_SYMBOL` symbol property, so the call above + will return `undefined` and we want that represented in the type + */ InternalQueryReference | undefined; + + return wrapHook( + "useQueryRefHandlers", + _useQueryRefHandlers, + unwrapped ? + unwrapped["observable"] + // in the case of a "transported" queryRef object, we need to use the + // client that's available to us at the current position in the React tree + // that ApolloClient will then have the job to recreate a real queryRef from + // the transported object + : useApolloClient() + )(queryRef); +} + +function _useQueryRefHandlers< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + queryRef: QueryReference ): UseQueryRefHandlersResult { const [previousQueryRef, setPreviousQueryRef] = React.useState(queryRef); const [wrappedQueryRef, setWrappedQueryRef] = React.useState(queryRef); diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 6c1ff70d4e8..6add98a094b 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -4,12 +4,16 @@ import { unwrapQueryRef, updateWrappedQueryRef, } from "../internal/index.js"; -import type { QueryReference } from "../internal/index.js"; +import type { + InternalQueryReference, + QueryReference, +} from "../internal/index.js"; import { __use, wrapHook } from "./internal/index.js"; import { toApolloError } from "./useSuspenseQuery.js"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import type { ApolloError } from "../../errors/index.js"; import type { NetworkStatus } from "../../core/index.js"; +import { useApolloClient } from "./useApolloClient.js"; export interface UseReadQueryResult { /** @@ -39,10 +43,25 @@ export interface UseReadQueryResult { export function useReadQuery( queryRef: QueryReference ): UseReadQueryResult { + const unwrapped = unwrapQueryRef( + queryRef + ) satisfies InternalQueryReference as /* + by all rules of this codebase, this should never be undefined + but if `queryRef` is a transported object, it cannot have a + `QUERY_REFERENCE_SYMBOL` symbol property, so the call above + will return `undefined` and we want that represented in the type + */ InternalQueryReference | undefined; + return wrapHook( "useReadQuery", _useReadQuery, - unwrapQueryRef(queryRef)["observable"] + unwrapped ? + unwrapped["observable"] + // in the case of a "transported" queryRef object, we need to use the + // client that's available to us at the current position in the React tree + // that ApolloClient will then have the job to recreate a real queryRef from + // the transported object + : useApolloClient() )(queryRef); } diff --git a/src/react/internal/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts index 148dcc250e8..f3bd6395b8c 100644 --- a/src/react/internal/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -73,7 +73,7 @@ export interface QueryReference { * } * ``` * - * @alpha + * @since 3.9.0 */ toPromise(): Promise>; } diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index e96e7825395..606ca5e2101 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -164,7 +164,6 @@ export interface PreloadQueryFunction { * const preloadQuery = createQueryPreloader(client); * ``` * @since 3.9.0 - * @alpha */ export function createQueryPreloader( client: ApolloClient diff --git a/src/testing/experimental/__tests__/createTestSchema.test.tsx b/src/testing/experimental/__tests__/createTestSchema.test.tsx new file mode 100644 index 00000000000..99b579ddde2 --- /dev/null +++ b/src/testing/experimental/__tests__/createTestSchema.test.tsx @@ -0,0 +1,1259 @@ +import * as React from "react"; +import { + ApolloClient, + ApolloError, + InMemoryCache, + gql, +} from "../../../core/index.js"; +import type { TypedDocumentNode } from "../../../core/index.js"; +import { + Profiler, + createProfiler, + renderWithClient, + spyOnConsole, +} from "../../internal/index.js"; +import { createTestSchema } from "../createTestSchema.js"; +import { GraphQLError, buildSchema } from "graphql"; +import type { UseSuspenseQueryResult } from "../../../react/index.js"; +import { useMutation, useSuspenseQuery } from "../../../react/index.js"; +import userEvent from "@testing-library/user-event"; +import { act, screen } from "@testing-library/react"; +import { createSchemaFetch } from "../createSchemaFetch.js"; +import { + FallbackProps, + ErrorBoundary as ReactErrorBoundary, +} from "react-error-boundary"; +import { InvariantError } from "ts-invariant"; + +const typeDefs = /* GraphQL */ ` + type User { + id: ID! + age: Int! + name: String! + image: UserImage! + book: Book! + } + + type Author { + _id: ID! + name: String! + book: Book! + } + + union UserImage = UserImageSolidColor | UserImageURL + + type UserImageSolidColor { + color: String! + } + + type UserImageURL { + url: String! + } + + scalar Date + + interface Book { + id: ID! + title: String + publishedAt: Date + } + + type TextBook implements Book { + id: ID! + title: String + publishedAt: Date + text: String + } + + type ColoringBook implements Book { + id: ID! + title: String + publishedAt: Date + colors: [String] + } + + type Query { + viewer: User! + userById(id: ID!): User! + author: Author! + } + + type Mutation { + changeViewerName(newName: String!): User! + } +`; + +const schemaWithTypeDefs = buildSchema(typeDefs); + +const uri = "https://localhost:3000/graphql"; + +function createDefaultProfiler() { + return createProfiler({ + initialSnapshot: { + result: null as UseSuspenseQueryResult | null, + }, + }); +} + +function createErrorProfiler() { + return createProfiler({ + initialSnapshot: { + error: null as Error | null, + result: null as UseSuspenseQueryResult | null, + }, + }); +} + +function createTrackedErrorComponents( + Profiler: Profiler +) { + function ErrorFallback({ error }: FallbackProps) { + Profiler.mergeSnapshot({ error } as Partial); + + return
Error
; + } + + function ErrorBoundary({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + return { ErrorBoundary }; +} + +interface ViewerQueryData { + viewer: { + id: string; + name: string; + age: number; + book: { + id: string; + title: string; + publishedAt: string; + }; + }; +} + +describe("schema proxy", () => { + const schema = createTestSchema(schemaWithTypeDefs, { + resolvers: { + Query: { + viewer: () => ({ + name: "Jane Doe", + book: { + text: "Hello World", + title: "The Book", + }, + }), + }, + Book: { + __resolveType: (obj) => { + if ("text" in obj) { + return "TextBook"; + } + if ("colors" in obj) { + return "ColoringBook"; + } + throw new Error("Could not resolve type"); + }, + }, + }, + scalars: { + ID: () => "1", + Int: () => 42, + String: () => "String", + Date: () => new Date("January 1, 2024 01:00:00").toJSON().split("T")[0], + }, + }); + + it("mocks scalars and resolvers", async () => { + const Profiler = createDefaultProfiler(); + + using _fetch = createSchemaFetch(schema).mockGlobal(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + uri, + }); + + const query: TypedDocumentNode = gql` + query { + viewer { + id + name + age + book { + id + title + publishedAt + } + } + } + `; + + const Fallback = () => { + return
Loading...
; + }; + + const App = () => { + return ( + }> + + + ); + }; + + const Child = () => { + const result = useSuspenseQuery(query); + + Profiler.mergeSnapshot({ + result, + }); + + return
Hello
; + }; + + renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + name: "Jane Doe", + book: { + __typename: "TextBook", + id: "1", + publishedAt: "2024-01-01", + title: "The Book", + }, + }, + }); + } + }); + + it("allows schema forking with .fork", async () => { + const forkedSchema = schema.fork({ + resolvers: { + Query: { + viewer: () => ({ + book: { + colors: ["red", "blue", "green"], + title: "The Book", + }, + }), + }, + }, + }); + + const Profiler = createDefaultProfiler(); + + using _fetch = createSchemaFetch(forkedSchema).mockGlobal(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + uri, + }); + + const query: TypedDocumentNode = gql` + query ViewerQuery { + viewer { + id + name + age + book { + id + title + publishedAt + } + } + } + `; + + const Fallback = () => { + return
Loading...
; + }; + + const App = () => { + return ( + }> + + + ); + }; + + const Child = () => { + const result = useSuspenseQuery(query); + + Profiler.mergeSnapshot({ + result, + } as Partial<{}>); + + return
Hello
; + }; + + renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + // In our resolvers defined in this test, we omit name so it uses + // the scalar default mock + name: "String", + book: { + // locally overrode the resolver for the book field + __typename: "ColoringBook", + id: "1", + publishedAt: "2024-01-01", + title: "The Book", + }, + }, + }); + } + }); + + it("schema.fork does not pollute the original schema", async () => { + const Profiler = createDefaultProfiler(); + + schema.fork({ + resolvers: { + Query: { + viewer: () => ({ + book: { + colors: ["red", "blue", "green"], + title: "The Book", + }, + }), + }, + }, + }); + + using _fetch = createSchemaFetch(schema).mockGlobal(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + uri, + }); + + const query: TypedDocumentNode = gql` + query { + viewer { + id + name + age + book { + id + title + publishedAt + } + } + } + `; + + const Fallback = () => { + return
Loading...
; + }; + + const App = () => { + return ( + }> + + + ); + }; + + const Child = () => { + const result = useSuspenseQuery(query); + + Profiler.mergeSnapshot({ + result, + } as Partial<{}>); + + return
Hello
; + }; + + renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + name: "Jane Doe", + book: { + __typename: "TextBook", + id: "1", + publishedAt: "2024-01-01", + title: "The Book", + }, + }, + }); + } + }); + + it("allows you to call .fork without providing resolvers", async () => { + const forkedSchema = schema.fork(); + + forkedSchema.add({ + resolvers: { + Query: { + viewer: () => ({ + book: { + colors: ["red", "blue", "green"], + title: "The Book", + }, + }), + }, + }, + }); + + const Profiler = createDefaultProfiler(); + + using _fetch = createSchemaFetch(forkedSchema).mockGlobal(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + uri, + }); + + const query: TypedDocumentNode = gql` + query { + viewer { + id + name + age + book { + id + title + publishedAt + } + } + } + `; + + const Fallback = () => { + return
Loading...
; + }; + + const App = () => { + return ( + }> + + + ); + }; + + const Child = () => { + const result = useSuspenseQuery(query); + + Profiler.mergeSnapshot({ + result, + } as Partial<{}>); + + return
Hello
; + }; + + renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + // since we called .add and provided a new `viewer` resolver + // _without_ providing the viewer.name field in the response data, + // it renders with the default scalar mock for String + name: "String", + book: { + __typename: "ColoringBook", + id: "1", + publishedAt: "2024-01-01", + title: "The Book", + }, + }, + }); + } + }); + + it("handles mutations", async () => { + const query: TypedDocumentNode = gql` + query { + viewer { + id + name + age + book { + id + title + publishedAt + } + } + } + `; + + let name = "Jane Doe"; + + const forkedSchema = schema.fork({ + resolvers: { + Query: { + viewer: () => ({ + book: { + text: "Hello World", + title: "The Book", + }, + }), + }, + User: { + name: () => name, + }, + Mutation: { + changeViewerName: (_: any, { newName }: { newName: string }) => { + name = newName; + return {}; + }, + }, + }, + }); + + const Profiler = createDefaultProfiler(); + + using _fetch = createSchemaFetch(forkedSchema).mockGlobal(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + uri, + }); + + const mutation = gql` + mutation { + changeViewerName(newName: "Alexandre") { + id + name + } + } + `; + + const Fallback = () => { + return
Loading...
; + }; + + const App = () => { + return ( + }> + + + ); + }; + + const Child = () => { + const result = useSuspenseQuery(query); + const [changeViewerName] = useMutation(mutation); + + Profiler.mergeSnapshot({ + result, + } as Partial<{}>); + + return ( +
+ + Hello +
+ ); + }; + + const user = userEvent.setup(); + + renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + name: "Jane Doe", + book: { + // locally overrode the resolver for the book field + __typename: "TextBook", + id: "1", + publishedAt: "2024-01-01", + title: "The Book", + }, + }, + }); + } + + await act(() => user.click(screen.getByText("Change name"))); + + // initial suspended render + await Profiler.takeRender(); + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + name: "Alexandre", + book: { + // locally overrode the resolver for the book field + __typename: "TextBook", + id: "1", + publishedAt: "2024-01-01", + title: "The Book", + }, + }, + }); + } + }); + + it("returns GraphQL errors", async () => { + using _consoleSpy = spyOnConsole("error"); + const query: TypedDocumentNode = gql` + query { + viewer { + id + name + age + book { + id + title + publishedAt + } + } + } + `; + + let name = "Jane Doe"; + + const forkedSchema = schema.fork({ + resolvers: { + Query: { + viewer: () => ({ + book: { + // text: "Hello World", <- this will cause a validation error + title: "The Book", + }, + }), + }, + User: { + name: () => name, + }, + }, + }); + + const Profiler = createErrorProfiler(); + + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); + + using _fetch = createSchemaFetch(forkedSchema).mockGlobal(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + uri, + }); + + const Fallback = () => { + return
Loading...
; + }; + + const App = () => { + return ( + }> + + + + + ); + }; + + const Child = () => { + const result = useSuspenseQuery(query); + + Profiler.mergeSnapshot({ + result, + } as Partial<{}>); + + return
Hello
; + }; + + renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.error).toEqual( + new ApolloError({ + graphQLErrors: [new GraphQLError("Could not resolve type")], + }) + ); + } + }); + + it("validates schema by default and returns validation errors", async () => { + using _consoleSpy = spyOnConsole("error"); + const query: TypedDocumentNode = gql` + query { + viewer { + id + name + age + book { + id + title + publishedAt + } + } + } + `; + + // invalid schema + const forkedSchema = { foo: "bar" }; + + const Profiler = createErrorProfiler(); + + const { ErrorBoundary } = createTrackedErrorComponents(Profiler); + + // @ts-expect-error - we're intentionally passing an invalid schema + using _fetch = createSchemaFetch(forkedSchema).mockGlobal(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + uri, + }); + + const Fallback = () => { + return
Loading...
; + }; + + const App = () => { + return ( + }> + + + + + ); + }; + + const Child = () => { + const result = useSuspenseQuery(query); + + Profiler.mergeSnapshot({ + result, + } as Partial<{}>); + + return
Hello
; + }; + + renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.error).toEqual( + new ApolloError({ + graphQLErrors: [ + new GraphQLError('Expected { foo: "bar" } to be a GraphQL schema.'), + ], + }) + ); + } + }); + + it("preserves resolvers from previous calls to .add on subsequent calls to .fork", async () => { + let name = "Virginia"; + + const schema = createTestSchema(schemaWithTypeDefs, { + resolvers: { + Query: { + viewer: () => ({ + name, + book: { + text: "Hello World", + title: "The Book", + }, + }), + }, + Book: { + __resolveType: (obj) => { + if ("text" in obj) { + return "TextBook"; + } + if ("colors" in obj) { + return "ColoringBook"; + } + throw new Error("Could not resolve type"); + }, + }, + }, + scalars: { + ID: () => "1", + Int: () => 42, + String: () => "String", + Date: () => new Date("January 1, 2024 01:00:00").toJSON().split("T")[0], + }, + }); + + schema.add({ + resolvers: { + Query: { + viewer: () => ({ + book: { + colors: ["red", "blue", "green"], + title: "A New Book", + }, + }), + }, + }, + }); + + schema.add({ + resolvers: { + User: { + name: () => name, + }, + }, + }); + + // should preserve resolvers from previous calls to .add + const forkedSchema = schema.fork({ + resolvers: { + Mutation: { + changeViewerName: (_: any, { newName }: { newName: string }) => { + name = newName; + return {}; + }, + }, + }, + }); + + const Profiler = createDefaultProfiler(); + + using _fetch = createSchemaFetch(forkedSchema).mockGlobal(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + uri, + }); + + const query: TypedDocumentNode = gql` + query { + viewer { + id + name + age + book { + id + title + publishedAt + ... on ColoringBook { + colors + } + } + } + } + `; + + const mutation = gql` + mutation { + changeViewerName(newName: "Alexandre") { + id + name + } + } + `; + + const Fallback = () => { + return
Loading...
; + }; + + const App = () => { + return ( + }> + + + ); + }; + + const Child = () => { + const result = useSuspenseQuery(query); + const [changeViewerName] = useMutation(mutation); + + Profiler.mergeSnapshot({ + result, + } as Partial<{}>); + + return ( +
+ + Hello +
+ ); + }; + + const user = userEvent.setup(); + + renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + name: "Virginia", + book: { + __typename: "ColoringBook", + colors: ["red", "blue", "green"], + id: "1", + publishedAt: "2024-01-01", + title: "A New Book", + }, + }, + }); + } + + await act(() => user.click(screen.getByText("Change name"))); + + await Profiler.takeRender(); + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + name: "Alexandre", + book: { + __typename: "ColoringBook", + colors: ["red", "blue", "green"], + id: "1", + publishedAt: "2024-01-01", + title: "A New Book", + }, + }, + }); + } + }); + + it("resets the schema with schema.reset()", async () => { + const resetTestSchema = createTestSchema(schema, { + resolvers: { + Query: { + viewer: () => ({ + book: { + text: "Hello World", + title: "Orlando: A Biography", + }, + }), + }, + Book: { + __resolveType: (obj) => { + if ("text" in obj) { + return "TextBook"; + } + if ("colors" in obj) { + return "ColoringBook"; + } + throw new Error("Could not resolve type"); + }, + }, + }, + }); + const Profiler = createDefaultProfiler(); + + resetTestSchema.add({ + resolvers: { + Query: { + viewer: () => ({ + book: { + text: "Hello World", + title: "The Waves", + }, + }), + }, + }, + }); + + using _fetch = createSchemaFetch(resetTestSchema).mockGlobal(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + uri, + }); + + const query: TypedDocumentNode = gql` + query { + viewer { + id + name + age + book { + id + title + publishedAt + ... on ColoringBook { + colors + } + } + } + } + `; + + const Fallback = () => { + return
Loading...
; + }; + + const App = () => { + return ( + }> + + + ); + }; + + const Child = () => { + const result = useSuspenseQuery(query); + + Profiler.mergeSnapshot({ + result, + } as Partial<{}>); + + return ( +
+ Hello +
+ ); + }; + + renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + name: "String", + book: { + __typename: "TextBook", + id: "1", + publishedAt: "2024-01-01", + // value set in this test with .add + title: "The Waves", + }, + }, + }); + } + + resetTestSchema.reset(); + + const user = userEvent.setup(); + + await act(() => user.click(screen.getByText("Refetch"))); + + // initial suspended render + await Profiler.takeRender(); + + { + const { snapshot } = await Profiler.takeRender(); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + name: "String", + book: { + __typename: "TextBook", + id: "1", + publishedAt: "2024-01-01", + // original value + title: "Orlando: A Biography", + }, + }, + }); + } + }); + + it("createSchemaFetch respects min and max delay", async () => { + const Profiler = createDefaultProfiler(); + + const minDelay = 1500; + const maxDelay = 2000; + + using _fetch = createSchemaFetch(schema, { + delay: { min: minDelay, max: maxDelay }, + }).mockGlobal(); + + const client = new ApolloClient({ + cache: new InMemoryCache(), + uri, + }); + + const query: TypedDocumentNode = gql` + query { + viewer { + id + name + age + book { + id + title + publishedAt + } + } + } + `; + + const Fallback = () => { + return
Loading...
; + }; + + const App = () => { + return ( + }> + + + ); + }; + + const Child = () => { + const result = useSuspenseQuery(query); + + Profiler.mergeSnapshot({ + result, + } as Partial<{}>); + + return
Hello
; + }; + + renderWithClient(, { + client, + wrapper: Profiler, + }); + + // initial suspended render + await Profiler.takeRender(); + + await expect(Profiler).not.toRerender({ timeout: minDelay - 100 }); + + { + const { snapshot } = await Profiler.takeRender({ + // This timeout doesn't start until after our `minDelay - 100` + // timeout above, so we don't have to wait the full `maxDelay` + // here. + // Instead we can just wait for the difference between `maxDelay` + // and `minDelay`, plus a bit to prevent flakiness. + timeout: maxDelay - minDelay + 110, + }); + + expect(snapshot.result?.data).toEqual({ + viewer: { + __typename: "User", + age: 42, + id: "1", + name: "Jane Doe", + book: { + __typename: "TextBook", + id: "1", + publishedAt: "2024-01-01", + title: "The Book", + }, + }, + }); + } + }); + + it("should call invariant.error if min delay is greater than max delay", async () => { + await expect(async () => { + createSchemaFetch(schema, { + delay: { min: 3000, max: 1000 }, + }); + }).rejects.toThrow( + new InvariantError( + "Please configure a minimum delay that is less than the maximum delay. The default minimum delay is 3ms." + ) + ); + }); +}); diff --git a/src/testing/experimental/createSchemaFetch.ts b/src/testing/experimental/createSchemaFetch.ts new file mode 100644 index 00000000000..5c03ea0da0d --- /dev/null +++ b/src/testing/experimental/createSchemaFetch.ts @@ -0,0 +1,110 @@ +import { execute, validate } from "graphql"; +import type { GraphQLError, GraphQLSchema } from "graphql"; +import { ApolloError, gql } from "../../core/index.js"; +import { withCleanup } from "../internal/index.js"; +import { wait } from "../core/wait.js"; + +/** + * A function that accepts a static `schema` and a `mockFetchOpts` object and + * returns a disposable object with `mock` and `restore` functions. + * + * The `mock` function is a mock fetch function that is set on the global + * `window` object. This function intercepts any fetch requests and + * returns a response by executing the operation against the provided schema. + * + * The `restore` function is a cleanup function that will restore the previous + * `fetch`. It is automatically called if the function's return value is + * declared with `using`. If your environment does not support the language + * feature `using`, you should manually invoke the `restore` function. + * + * @param schema - A `GraphQLSchema`. + * @param mockFetchOpts - Configuration options. + * @returns An object with both `mock` and `restore` functions. + * + * @example + * ```js + * using _fetch = createSchemaFetch(schema); // automatically restores fetch after exiting the block + * + * const { restore } = createSchemaFetch(schema); + * restore(); // manually restore fetch if `using` is not supported + * ``` + * @since 3.10.0 + * @alpha + */ +const createSchemaFetch = ( + schema: GraphQLSchema, + mockFetchOpts: { + validate?: boolean; + delay?: { min: number; max: number }; + } = { validate: true } +) => { + const prevFetch = window.fetch; + const delayMin = mockFetchOpts.delay?.min ?? 3; + const delayMax = mockFetchOpts.delay?.max ?? delayMin + 2; + + if (delayMin > delayMax) { + throw new Error( + "Please configure a minimum delay that is less than the maximum delay. The default minimum delay is 3ms." + ); + } + + const mockFetch: (uri?: any, options?: any) => Promise = async ( + _uri, + options + ) => { + if (delayMin > 0) { + const randomDelay = Math.random() * (delayMax - delayMin) + delayMin; + await wait(randomDelay); + } + + const body = JSON.parse(options.body); + const document = gql(body.query); + + if (mockFetchOpts.validate) { + let validationErrors: readonly Error[] = []; + + try { + validationErrors = validate(schema, document); + } catch (e) { + validationErrors = [ + new ApolloError({ graphQLErrors: [e as GraphQLError] }), + ]; + } + + if (validationErrors?.length > 0) { + return new Response(JSON.stringify({ errors: validationErrors })); + } + } + + const result = await execute({ + schema, + document, + variableValues: body.variables, + operationName: body.operationName, + }); + + const stringifiedResult = JSON.stringify(result); + + return new Response(stringifiedResult); + }; + + function mockGlobal() { + window.fetch = mockFetch; + + const restore = () => { + if (window.fetch === mockFetch) { + window.fetch = prevFetch; + } + }; + + return withCleanup({ restore }, restore); + } + + return Object.assign(mockFetch, { + mockGlobal, + // if https://github.com/rbuckton/proposal-using-enforcement lands + // [Symbol.enter]: mockGlobal + }); +}; + +export { createSchemaFetch }; diff --git a/src/testing/experimental/createTestSchema.ts b/src/testing/experimental/createTestSchema.ts new file mode 100644 index 00000000000..5a4002c2902 --- /dev/null +++ b/src/testing/experimental/createTestSchema.ts @@ -0,0 +1,134 @@ +import type { GraphQLSchema } from "graphql"; +import { addResolversToSchema } from "@graphql-tools/schema"; +import { mergeResolvers } from "@graphql-tools/merge"; +import { createMockSchema } from "./graphql-tools/utils.js"; +import type { Resolvers } from "../../core/types.js"; + +type ProxiedSchema = GraphQLSchema & TestSchemaFns; + +interface TestSchemaFns { + add: (addOptions: { resolvers: Resolvers }) => ProxiedSchema; + fork: (forkOptions?: { resolvers?: Resolvers }) => ProxiedSchema; + reset: () => void; +} + +interface TestSchemaOptions { + resolvers: Resolvers; + scalars?: { [key: string]: any }; +} + +/** + * A function that creates a [Proxy object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy) + * around a given `schema` with `resolvers`. This proxied schema can be used to + * progressively layer resolvers on top of the original schema using the `add` + * method. The `fork` method can be used to create a new proxied schema which + * can be modified independently of the original schema. `reset` will restore + * resolvers to the original proxied schema. + * + * @param schema - A `GraphQLSchema`. + * @param options - An `options` object that accepts `scalars` and `resolvers` objects. + * @returns A `ProxiedSchema` with `add`, `fork` and `reset` methods. + * + * @example + * ```js + * + * const schema = createTestSchema(schemaWithTypeDefs, { + * resolvers: { + Query: { + writer: () => ({ + name: "Ada Lovelace", + }), + } + }, + scalars: { + ID: () => "1", + Int: () => 36, + String: () => "String", + Date: () => new Date("December 10, 1815 01:00:00").toJSON().split("T")[0], + } + }); + * ``` + * @since 3.9.0 + * @alpha + */ +const createTestSchema = ( + schemaWithTypeDefs: GraphQLSchema, + options: TestSchemaOptions +): ProxiedSchema => { + let targetResolvers = { ...options.resolvers }; + let targetSchema = addResolversToSchema({ + schema: createMockSchema(schemaWithTypeDefs, options.scalars ?? {}), + resolvers: targetResolvers, + }); + + const fns: TestSchemaFns = { + add: ({ resolvers: newResolvers }) => { + // @ts-ignore TODO(fixme): IResolvers type does not play well with our Resolvers + targetResolvers = mergeResolvers([targetResolvers, newResolvers]); + + targetSchema = addResolversToSchema({ + schema: targetSchema, + resolvers: targetResolvers, + }); + + return targetSchema as ProxiedSchema; + }, + + fork: ({ resolvers: newResolvers } = {}) => { + return createTestSchema(targetSchema, { + // @ts-ignore TODO(fixme): IResolvers type does not play well with our Resolvers + resolvers: + mergeResolvers([targetResolvers, newResolvers]) ?? targetResolvers, + scalars: options.scalars, + }); + }, + + reset: () => { + targetSchema = addResolversToSchema({ + schema: schemaWithTypeDefs, + resolvers: options.resolvers, + }); + }, + }; + + const schema = new Proxy(targetSchema, { + get(_target, p) { + if (p in fns) { + return Reflect.get(fns, p); + } + + // An optimization that eliminates round-trips through the proxy + // on class methods invoked via `this` on a base class instance wrapped by + // the proxy. + // + // For example, consider the following class: + // + // class Base { + // foo(){ + // this.bar() + // } + // bar(){ + // ... + // } + // } + // + // Calling `proxy.foo()` would call `foo` with `this` being the proxy. + // This would result in calling `proxy.bar()` which would again invoke + // the proxy to resolve `bar` and call that method. + // + // Instead, calls to `proxy.foo()` should result in a call to + // `innerObject.foo()` with a `this` of `innerObject`, and that call + // should directly call `innerObject.bar()`. + + const property = Reflect.get(targetSchema, p); + if (typeof property === "function") { + return property.bind(targetSchema); + } + return property; + }, + }); + + return schema as ProxiedSchema; +}; + +export { createTestSchema }; diff --git a/src/testing/experimental/graphql-tools/LICENSE b/src/testing/experimental/graphql-tools/LICENSE new file mode 100644 index 00000000000..f5940526b77 --- /dev/null +++ b/src/testing/experimental/graphql-tools/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 The Guild, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/src/testing/experimental/graphql-tools/utils.test.ts b/src/testing/experimental/graphql-tools/utils.test.ts new file mode 100644 index 00000000000..0d7a9c63fac --- /dev/null +++ b/src/testing/experimental/graphql-tools/utils.test.ts @@ -0,0 +1,227 @@ +// Originally from @graphql-tools/mock +// https://github.com/ardatan/graphql-tools/blob/4b56b04d69b02919f6c5fa4f97d33da63f36e8c8/packages/mock/tests/addMocksToSchema.spec.ts + +import { buildSchema, graphql } from "graphql"; +import { createMockSchema } from "./utils.js"; + +const mockDate = new Date().toJSON().split("T")[0]; + +const mocks = { + Int: () => 6, + Float: () => 22.1, + String: () => "string", + ID: () => "id", + Date: () => mockDate, +}; + +const typeDefs = /* GraphQL */ ` + type User { + id: ID! + age: Int! + name: String! + image: UserImage! + book: Book! + } + + type Author { + _id: ID! + name: String! + book: Book! + } + + union UserImage = UserImageSolidColor | UserImageURL + + type UserImageSolidColor { + color: String! + } + + type UserImageURL { + url: String! + } + + scalar Date + + interface Book { + id: ID! + title: String + publishedAt: Date + } + + type TextBook implements Book { + id: ID! + title: String + publishedAt: Date + text: String + } + + type ColoringBook implements Book { + id: ID! + title: String + publishedAt: Date + colors: [String] + } + + type Query { + viewer: User! + userById(id: ID!): User! + author: Author! + } + + type Mutation { + changeViewerName(newName: String!): User! + } +`; + +const schema = buildSchema(typeDefs); + +describe("addMocksToSchema", () => { + it("basic", async () => { + const query = /* GraphQL */ ` + query { + viewer { + id + name + age + } + } + `; + + const mockedSchema = createMockSchema(schema, mocks); + + const { data, errors } = await graphql({ + schema: mockedSchema, + source: query, + }); + + expect(errors).not.toBeDefined(); + expect(data).toBeDefined(); + + const viewerData = data?.["viewer"] as any; + expect(typeof viewerData["id"]).toBe("string"); + expect(typeof viewerData["name"]).toBe("string"); + expect(typeof viewerData["age"]).toBe("number"); + + const { data: data2 } = await graphql({ + schema: mockedSchema, + source: query, + }); + + const viewerData2 = data2?.["viewer"] as any; + + expect(viewerData2["id"]).toEqual(viewerData["id"]); + }); + + it("handle _id key field", async () => { + const query = /* GraphQL */ ` + query { + author { + _id + name + } + } + `; + const mockedSchema = createMockSchema(schema, mocks); + const { data, errors } = await graphql({ + schema: mockedSchema, + source: query, + }); + + expect(errors).not.toBeDefined(); + expect(data).toBeDefined(); + const viewerData = data?.["author"] as any; + expect(typeof viewerData["_id"]).toBe("string"); + expect(typeof viewerData["name"]).toBe("string"); + + const { data: data2 } = await graphql({ + schema: mockedSchema, + source: query, + }); + + const viewerData2 = data2?.["author"] as any; + + expect(viewerData2["_id"]).toEqual(viewerData["_id"]); + }); + + it("should handle union type", async () => { + const query = /* GraphQL */ ` + query { + viewer { + image { + __typename + ... on UserImageURL { + url + } + ... on UserImageSolidColor { + color + } + } + } + } + `; + + const mockedSchema = createMockSchema(schema, mocks); + + const { data, errors } = await graphql({ + schema: mockedSchema, + source: query, + }); + + expect(errors).not.toBeDefined(); + expect(data).toBeDefined(); + expect((data!["viewer"] as any)["image"]["__typename"]).toBeDefined(); + }); + + it("should handle interface type", async () => { + const query = /* GraphQL */ ` + query { + viewer { + book { + title + __typename + ... on TextBook { + text + } + ... on ColoringBook { + colors + } + } + } + } + `; + + const mockedSchema = createMockSchema(schema, mocks); + + const { data, errors } = await graphql({ + schema: mockedSchema, + source: query, + }); + + expect(errors).not.toBeDefined(); + expect(data).toBeDefined(); + expect((data!["viewer"] as any)["book"]["__typename"]).toBeDefined(); + }); + + it("should handle custom scalars", async () => { + const query = /* GraphQL */ ` + query { + viewer { + book { + title + publishedAt + } + } + } + `; + + const mockedSchema = createMockSchema(schema, mocks); + + const { data, errors } = await graphql({ + schema: mockedSchema, + source: query, + }); + + expect(errors).not.toBeDefined(); + expect(data).toBeDefined(); + expect((data!["viewer"] as any)["book"]["publishedAt"]).toBe(mockDate); + }); +}); diff --git a/src/testing/experimental/graphql-tools/utils.ts b/src/testing/experimental/graphql-tools/utils.ts new file mode 100644 index 00000000000..4ff08d7ab0f --- /dev/null +++ b/src/testing/experimental/graphql-tools/utils.ts @@ -0,0 +1,251 @@ +import type { + GraphQLFieldResolver, + GraphQLObjectType, + GraphQLOutputType, + GraphQLSchema, +} from "graphql"; + +import { + GraphQLInterfaceType, + GraphQLString, + GraphQLUnionType, + defaultFieldResolver, + getNullableType, + isAbstractType, + isEnumType, + isInterfaceType, + isListType, + isObjectType, + isScalarType, + isUnionType, +} from "graphql"; + +import { isNonNullObject } from "../../../utilities/index.js"; +import { MapperKind, mapSchema, getRootTypeNames } from "@graphql-tools/utils"; + +// Taken from @graphql-tools/mock: +// https://github.com/ardatan/graphql-tools/blob/4b56b04d69b02919f6c5fa4f97d33da63f36e8c8/packages/mock/src/utils.ts#L20 +const takeRandom = (arr: T[]) => arr[Math.floor(Math.random() * arr.length)]; + +/** + * A function that accepts a static `schema` and a `mocks` object for specifying + * default scalar mocks and returns a `GraphQLSchema`. + * + * @param staticSchema - A static `GraphQLSchema`. + * @param mocks - An object containing scalar mocks. + * @returns A `GraphQLSchema` with scalar mocks. + * + * @example + * ```js + * const mockedSchema = createMockSchema(schema, { + ID: () => "1", + Int: () => 42, + String: () => "String", + Date: () => new Date("January 1, 2024 01:00:00").toJSON().split("T")[0], + }); + * ``` + * @since 3.10.0 + * @alpha + */ +const createMockSchema = ( + staticSchema: GraphQLSchema, + mocks: { [key: string]: any } +) => { + // Taken from @graphql-tools/mock: + // https://github.com/ardatan/graphql-tools/blob/5ed60e44f94868f976cd28fe1b6a764fb146bbe9/packages/mock/src/MockStore.ts#L613 + const getType = (typeName: string) => { + const type = staticSchema.getType(typeName); + + if (!type || !(isObjectType(type) || isInterfaceType(type))) { + throw new Error( + `${typeName} does not exist on schema or is not an object or interface` + ); + } + + return type; + }; + + // Taken from @graphql-tools/mock: + // https://github.com/ardatan/graphql-tools/blob/5ed60e44f94868f976cd28fe1b6a764fb146bbe9/packages/mock/src/MockStore.ts#L597 + const getFieldType = (typeName: string, fieldName: string) => { + if (fieldName === "__typename") { + return GraphQLString; + } + + const type = getType(typeName); + + const field = type.getFields()[fieldName]; + + if (!field) { + throw new Error(`${fieldName} does not exist on type ${typeName}`); + } + + return field.type; + }; + + // Taken from @graphql-tools/mock: + // https://github.com/ardatan/graphql-tools/blob/5ed60e44f94868f976cd28fe1b6a764fb146bbe9/packages/mock/src/MockStore.ts#L527 + const generateValueFromType = (fieldType: GraphQLOutputType): unknown => { + const nullableType = getNullableType(fieldType); + + if (isScalarType(nullableType)) { + const mockFn = mocks[nullableType.name]; + + if (typeof mockFn !== "function") { + throw new Error(`No mock defined for type "${nullableType.name}"`); + } + + return mockFn(); + } else if (isEnumType(nullableType)) { + const mockFn = mocks[nullableType.name]; + + if (typeof mockFn === "function") return mockFn(); + + const values = nullableType.getValues().map((v) => v.value); + + return takeRandom(values); + } else if (isObjectType(nullableType)) { + return {}; + } else if (isListType(nullableType)) { + return [...new Array(2)].map(() => + generateValueFromType(nullableType.ofType) + ); + } else if (isAbstractType(nullableType)) { + const mock = mocks[nullableType.name]; + + let typeName: string; + + let values: { [key: string]: unknown } = {}; + + if (!mock) { + typeName = takeRandom( + staticSchema.getPossibleTypes(nullableType).map((t) => t.name) + ); + } else if (typeof mock === "function") { + const mockRes = mock(); + + if (mockRes === null) return null; + + if (!isNonNullObject(mockRes)) { + throw new Error( + `Value returned by the mock for ${nullableType.name} is not an object or null` + ); + } + + values = mockRes; + + if (typeof values["__typename"] !== "string") { + throw new Error( + `Please return a __typename in "${nullableType.name}"` + ); + } + + typeName = values["__typename"]; + } else if ( + isNonNullObject(mock) && + typeof mock["__typename"] === "function" + ) { + const mockRes = mock["__typename"](); + + if (typeof mockRes !== "string") { + throw new Error( + `'__typename' returned by the mock for abstract type ${nullableType.name} is not a string` + ); + } + + typeName = mockRes; + } else { + throw new Error(`Please return a __typename in "${nullableType.name}"`); + } + + return typeName; + } else { + throw new Error(`${nullableType} not implemented`); + } + }; + + // Taken from @graphql-tools/mock: + // https://github.com/ardatan/graphql-tools/blob/5ed60e44f94868f976cd28fe1b6a764fb146bbe9/packages/mock/src/utils.ts#L53 + const isRootType = (type: GraphQLObjectType, schema: GraphQLSchema) => { + const rootTypeNames = getRootTypeNames(schema); + + return rootTypeNames.has(type.name); + }; + + // Taken from @graphql-tools/mock: + // https://github.com/ardatan/graphql-tools/blob/5ed60e44f94868f976cd28fe1b6a764fb146bbe9/packages/mock/src/addMocksToSchema.ts#L123 + const mockResolver: GraphQLFieldResolver = ( + source, + args, + contex, + info + ) => { + const defaultResolvedValue = defaultFieldResolver( + source, + args, + contex, + info + ); + + // priority to default resolved value + if (defaultResolvedValue !== undefined) return defaultResolvedValue; + + // we have to handle the root mutation, root query and root subscription types + // differently, because no resolver is called at the root + if (isRootType(info.parentType, info.schema)) { + return { + typeName: info.parentType.name, + key: "ROOT", + fieldName: info.fieldName, + fieldArgs: args, + }; + } + + if (defaultResolvedValue === undefined) { + const fieldType = getFieldType(info.parentType.name, info.fieldName); + + return generateValueFromType(fieldType); + } + + return undefined; + }; + + // Taken from @graphql-tools/mock: + // https://github.com/ardatan/graphql-tools/blob/5ed60e44f94868f976cd28fe1b6a764fb146bbe9/packages/mock/src/addMocksToSchema.ts#L176 + return mapSchema(staticSchema, { + [MapperKind.OBJECT_FIELD]: (fieldConfig) => { + const newFieldConfig = { ...fieldConfig }; + + const oldResolver = fieldConfig.resolve; + + if (!oldResolver) { + newFieldConfig.resolve = mockResolver; + } + return newFieldConfig; + }, + + [MapperKind.ABSTRACT_TYPE]: (type) => { + if (type.resolveType != null && type.resolveType.length) { + return; + } + + const typeResolver = (typename: string) => { + return typename; + }; + + if (isUnionType(type)) { + return new GraphQLUnionType({ + ...type.toConfig(), + resolveType: typeResolver, + }); + } else { + return new GraphQLInterfaceType({ + ...type.toConfig(), + resolveType: typeResolver, + }); + } + }, + }); +}; + +export { createMockSchema }; diff --git a/src/testing/experimental/index.ts b/src/testing/experimental/index.ts new file mode 100644 index 00000000000..a7080de66d2 --- /dev/null +++ b/src/testing/experimental/index.ts @@ -0,0 +1,2 @@ +export { createTestSchema } from "./createTestSchema.js"; +export { createSchemaFetch } from "./createSchemaFetch.js"; diff --git a/src/testing/internal/profile/Render.tsx b/src/testing/internal/profile/Render.tsx index 0757392b26d..284594c9f51 100644 --- a/src/testing/internal/profile/Render.tsx +++ b/src/testing/internal/profile/Render.tsx @@ -124,7 +124,7 @@ export class RenderInstance implements Render { return (this._domSnapshot = body); } - get withinDOM() { + get withinDOM(): () => SyncScreen { const snapScreen = Object.assign(within(this.domSnapshot), { debug: ( ...[dom = this.domSnapshot, ...rest]: Parameters diff --git a/src/testing/internal/profile/profile.tsx b/src/testing/internal/profile/profile.tsx index 12e681ad7d2..b9e82619534 100644 --- a/src/testing/internal/profile/profile.tsx +++ b/src/testing/internal/profile/profile.tsx @@ -151,6 +151,9 @@ export function createProfiler({ let nextRender: Promise> | undefined; let resolveNextRender: ((render: Render) => void) | undefined; let rejectNextRender: ((error: unknown) => void) | undefined; + function resetNextRender() { + nextRender = resolveNextRender = rejectNextRender = undefined; + } const snapshotRef = { current: initialSnapshot }; const replaceSnapshot: ReplaceSnapshot = (snap) => { if (typeof snap === "function") { @@ -241,7 +244,7 @@ export function createProfiler({ }); rejectNextRender?.(error); } finally { - nextRender = resolveNextRender = rejectNextRender = undefined; + resetNextRender(); } }; @@ -340,13 +343,12 @@ export function createProfiler({ rejectNextRender = reject; }), new Promise>((_, reject) => - setTimeout( - () => - reject( - applyStackTrace(new WaitForRenderTimeoutError(), stackTrace) - ), - timeout - ) + setTimeout(() => { + reject( + applyStackTrace(new WaitForRenderTimeoutError(), stackTrace) + ); + resetNextRender(); + }, timeout) ), ]); } diff --git a/src/utilities/caching/caches.ts b/src/utilities/caching/caches.ts index 6acbdb88d34..a42cd24a06d 100644 --- a/src/utilities/caching/caches.ts +++ b/src/utilities/caching/caches.ts @@ -1,8 +1,15 @@ -import type { CommonCache } from "@wry/caches"; import { WeakCache, StrongCache } from "@wry/caches"; -const scheduledCleanup = new WeakSet>(); -function schedule(cache: CommonCache) { +interface CleanableCache { + size: number; + max?: number; + clean: () => void; +} +const scheduledCleanup = new WeakSet(); +function schedule(cache: CleanableCache) { + if (cache.size <= (cache.max || -1)) { + return; + } if (!scheduledCleanup.has(cache)) { scheduledCleanup.add(cache); setTimeout(() => { @@ -14,7 +21,7 @@ function schedule(cache: CommonCache) { /** * @internal * A version of WeakCache that will auto-schedule a cleanup of the cache when - * a new item is added. + * a new item is added and the cache reached maximum size. * Throttled to once per 100ms. * * @privateRemarks @@ -35,8 +42,9 @@ export const AutoCleanedWeakCache = function ( */ const cache = new WeakCache(max, dispose); cache.set = function (key: any, value: any) { - schedule(this); - return WeakCache.prototype.set.call(this, key, value); + const ret = WeakCache.prototype.set.call(this, key, value); + schedule(this as any as CleanableCache); + return ret; }; return cache; } as any as typeof WeakCache; @@ -48,7 +56,7 @@ export type AutoCleanedWeakCache = WeakCache; /** * @internal * A version of StrongCache that will auto-schedule a cleanup of the cache when - * a new item is added. + * a new item is added and the cache reached maximum size. * Throttled to once per 100ms. * * @privateRemarks @@ -69,8 +77,9 @@ export const AutoCleanedStrongCache = function ( */ const cache = new StrongCache(max, dispose); cache.set = function (key: any, value: any) { - schedule(this); - return StrongCache.prototype.set.call(this, key, value); + const ret = StrongCache.prototype.set.call(this, key, value); + schedule(this as any as CleanableCache); + return ret; }; return cache; } as any as typeof StrongCache;