diff --git a/.api-reports/api-report-core.api.md b/.api-reports/api-report-core.api.md index 53a5e866698..3a5051ef34b 100644 --- a/.api-reports/api-report-core.api.md +++ b/.api-reports/api-report-core.api.md @@ -1692,6 +1692,8 @@ export class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1711,6 +1713,8 @@ export class ObservableQuery>): ObservableSubscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1726,8 +1732,8 @@ export class ObservableQuery>): void; startPolling(pollInterval: number): void; stopPolling(): void; - subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1864,8 +1870,6 @@ class QueryInfo { // (undocumented) lastRequestId: number; // (undocumented) - listeners: Set; - // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) markReady(): NetworkStatus; @@ -1878,14 +1882,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1901,9 +1901,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -export type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1944,6 +1941,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2008,8 +2007,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2317,12 +2314,33 @@ class Stump extends Layer { } // @public (undocumented) -export type SubscribeToMoreOptions = { +export interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} + +// @public (undocumented) +export interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +export type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -2421,18 +2439,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +export interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} // @public (undocumented) -export interface UpdateQueryOptions { - // (undocumented) +export type UpdateQueryOptions = { variables?: TVariables; -} +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) export interface UriFunction { @@ -2508,11 +2530,10 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react.api.md b/.api-reports/api-report-react.api.md index aa55cb6c431..632a42dc1a9 100644 --- a/.api-reports/api-report-react.api.md +++ b/.api-reports/api-report-react.api.md @@ -373,6 +373,7 @@ type BackgroundQueryHookOptionsNoInfer = ApolloCache> extends MutationSharedOptions { client?: ApolloClient; + // @deprecated ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; @@ -885,6 +886,11 @@ TData }; } : never : never; +// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type From = StoreObject | Reference | FragmentType> | string | null; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -1042,7 +1048,9 @@ export interface LazyQueryHookExecOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; } @@ -1393,6 +1401,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1412,6 +1422,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1428,8 +1442,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1446,8 +1461,9 @@ export interface ObservableQueryFields>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -1537,8 +1553,6 @@ export interface PreloadQueryFunction { (query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg>): PreloadedQueryRef; } -// Warning: (ae-forgotten-export) The symbol "VariablesOption" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export type PreloadQueryOptions = { canonizeResults?: boolean; @@ -1588,7 +1602,9 @@ export interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1616,10 +1632,6 @@ class QueryInfo { }): this; // (undocumented) lastRequestId: number; - // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts - // - // (undocumented) - listeners: Set; // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) @@ -1633,14 +1645,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1662,9 +1670,6 @@ export interface QueryLazyOptions { variables?: TVariables; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1707,6 +1712,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1774,8 +1781,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2087,15 +2092,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreFunction = ObservableQueryFields["subscribeToMore"]; +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: Context; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: Context; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -2217,12 +2242,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -2316,8 +2351,6 @@ export function useFragment(options: Us // @public (undocumented) export interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; - // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts - // // (undocumented) from: StoreObject | Reference | FragmentType> | string | null; // (undocumented) @@ -2412,6 +2445,38 @@ export function useSubscription(options: UseSuspenseFragmentOptions & { + from: NonNullable>; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: null; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: From; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; + +// @public (undocumented) +export type UseSuspenseFragmentOptions = { + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: From; + optimistic?: boolean; + client?: ApolloClient; +} & VariablesOption>; + +// @public (undocumented) +export type UseSuspenseFragmentResult = { + data: MaybeMasked; +}; + // @public (undocumented) export function useSuspenseQuery, "variables">>(query: DocumentNode | TypedDocumentNode, options?: SuspenseQueryHookOptions, NoInfer_2> & TOptions): UseSuspenseQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? TOptions["skip"] extends boolean ? DeepPartial | undefined : DeepPartial : TOptions["skip"] extends boolean ? TData | undefined : TData, TVariables>; @@ -2472,11 +2537,11 @@ export interface UseSuspenseQueryResult = [ +export type VariablesOption = [ TVariables ] extends [never] ? { variables?: Record; -} : {} extends OnlyRequiredProperties ? { +} : Record extends OnlyRequiredProperties ? { variables?: TVariables; } : { variables: TVariables; @@ -2519,17 +2584,17 @@ interface WatchQueryOptions = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts client?: ApolloClient; + // @deprecated ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; @@ -1271,6 +1272,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1290,6 +1293,8 @@ class ObservableQuery>): Subscription_2; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1306,8 +1313,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1324,8 +1332,9 @@ interface ObservableQueryFields { reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -1416,7 +1425,9 @@ export interface QueryComponentOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1440,10 +1451,6 @@ class QueryInfo { }): this; // (undocumented) lastRequestId: number; - // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts - // - // (undocumented) - listeners: Set; // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) @@ -1457,14 +1464,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1480,9 +1483,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1525,6 +1525,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1592,8 +1594,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1838,12 +1838,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} + +// @public (undocumented) +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public @deprecated (undocumented) @@ -1950,12 +1973,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -2001,13 +2034,13 @@ interface WatchQueryOptions, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1244,6 +1246,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1260,8 +1266,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1278,8 +1285,9 @@ interface ObservableQueryFields { reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -1344,7 +1352,9 @@ interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1368,10 +1378,6 @@ class QueryInfo { }): this; // (undocumented) lastRequestId: number; - // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts - // - // (undocumented) - listeners: Set; // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) @@ -1385,14 +1391,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1408,9 +1410,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1453,6 +1452,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1520,8 +1521,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1791,12 +1790,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} + +// @public (undocumented) +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1870,12 +1892,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -1921,13 +1953,13 @@ interface WatchQueryOptions = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts client?: ApolloClient; + // @deprecated ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; @@ -1264,6 +1265,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1283,6 +1286,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1299,8 +1306,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1386,10 +1394,8 @@ export interface QueryControls void; // (undocumented) subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - // Warning: (ae-forgotten-export) The symbol "UpdateQueryOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) - updateQuery: (mapFn: (previousQueryResult: any, options: UpdateQueryOptions) => any) => void; + updateQuery: (mapFn: UpdateQueryMapFn) => void; // (undocumented) variables: TGraphQLVariables; } @@ -1413,10 +1419,6 @@ class QueryInfo { }): this; // (undocumented) lastRequestId: number; - // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts - // - // (undocumented) - listeners: Set; // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) @@ -1430,14 +1432,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1453,9 +1451,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1498,6 +1493,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1565,8 +1562,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1795,12 +1790,29 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1874,18 +1886,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} // @public (undocumented) -interface UpdateQueryOptions { - // (undocumented) +type UpdateQueryOptions = { variables?: TVariables; -} +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -1948,13 +1964,13 @@ export function withSubscription = ApolloCache> extends MutationSharedOptions { // Warning: (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts client?: ApolloClient; + // @deprecated ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; @@ -840,6 +841,11 @@ TData }; } : never : never; +// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type From = StoreObject | Reference | FragmentType> | string | null; + // @internal const getApolloCacheMemoryInternals: (() => { cache: { @@ -991,7 +997,9 @@ interface LazyQueryHookExecOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; } @@ -1337,6 +1345,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1356,6 +1366,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1372,8 +1386,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1390,8 +1405,9 @@ interface ObservableQueryFields { reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -1461,7 +1477,9 @@ const QUERY_REF_BRAND: unique symbol; interface QueryFunctionOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1491,10 +1509,6 @@ class QueryInfo { }): this; // (undocumented) lastRequestId: number; - // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts - // - // (undocumented) - listeners: Set; // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) @@ -1508,14 +1522,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1531,9 +1541,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1576,6 +1583,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1643,8 +1652,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1923,15 +1930,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreFunction = ObservableQueryFields["subscribeToMore"]; +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // Warning: (ae-forgotten-export) The symbol "BaseSubscriptionOptions" needs to be exported by the entry point index.d.ts @@ -2040,12 +2067,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -2140,8 +2177,6 @@ export function useFragment(options: Us // @public (undocumented) export interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; - // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts - // // (undocumented) from: StoreObject | Reference | FragmentType> | string | null; // (undocumented) @@ -2245,6 +2280,40 @@ export function useSubscription(options: UseSuspenseFragmentOptions & { + from: NonNullable>; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: null; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: From; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; + +// Warning: (ae-forgotten-export) The symbol "VariablesOption" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type UseSuspenseFragmentOptions = { + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: From; + optimistic?: boolean; + client?: ApolloClient; +} & VariablesOption>; + +// @public (undocumented) +export type UseSuspenseFragmentResult = { + data: MaybeMasked; +}; + // Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookOptions" needs to be exported by the entry point index.d.ts // // @public (undocumented) @@ -2306,6 +2375,17 @@ export interface UseSuspenseQueryResult; } +// @public (undocumented) +type VariablesOption = [ +TVariables +] extends [never] ? { + variables?: Record; +} : Record extends OnlyRequiredProperties ? { + variables?: TVariables; +} : { + variables: TVariables; +}; + // @public interface WatchFragmentOptions { fragment: DocumentNode | TypedDocumentNode; @@ -2343,17 +2423,17 @@ interface WatchQueryOptions; } +// @public (undocumented) +type FragmentCacheKey = [ +cacheId: string, +fragment: DocumentNode, +stringifiedVariables: string +]; + +// @public (undocumented) +interface FragmentKey { + // (undocumented) + __fragmentKey?: string; +} + // @public interface FragmentMap { // (undocumented) @@ -816,6 +829,43 @@ interface FragmentMap { // @public (undocumented) type FragmentMatcher = (rootValue: any, typeCondition: string, context: any) => boolean; +// @public (undocumented) +class FragmentReference> { + // Warning: (ae-forgotten-export) The symbol "FragmentReferenceOptions" needs to be exported by the entry point index.d.ts + constructor(client: ApolloClient, watchFragmentOptions: WatchFragmentOptions & { + from: string; + }, options: FragmentReferenceOptions); + // Warning: (ae-forgotten-export) The symbol "FragmentKey" needs to be exported by the entry point index.d.ts + // + // (undocumented) + readonly key: FragmentKey; + // Warning: (ae-forgotten-export) The symbol "Listener_2" needs to be exported by the entry point index.d.ts + // + // (undocumented) + listen(listener: Listener_2>): () => void; + // (undocumented) + readonly observable: Observable>; + // Warning: (ae-forgotten-export) The symbol "FragmentRefPromise" needs to be exported by the entry point index.d.ts + // + // (undocumented) + promise: FragmentRefPromise>; + // (undocumented) + retain(): () => void; +} + +// @public (undocumented) +interface FragmentReferenceOptions { + // (undocumented) + autoDisposeTimeoutMs?: number; + // (undocumented) + onDispose?: () => void; +} + +// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type FragmentRefPromise = PromiseWithState; + // @public (undocumented) type FragmentType = [ TData @@ -827,6 +877,11 @@ TData }; } : never : never; +// Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +type From = StoreObject | Reference | FragmentType> | string | null; + // @public (undocumented) interface FulfilledPromise extends Promise { // (undocumented) @@ -1036,6 +1091,9 @@ type IsStrictlyAny = UnionToIntersection> extends never ? true // @public (undocumented) type Listener = (promise: QueryRefPromise) => void; +// @public (undocumented) +type Listener_2 = (promise: FragmentRefPromise) => void; + // @public (undocumented) class LocalState { // Warning: (ae-forgotten-export) The symbol "LocalStateOptions" needs to be exported by the entry point index.d.ts @@ -1322,6 +1380,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1341,6 +1401,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1357,8 +1421,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1375,8 +1440,9 @@ interface ObservableQueryFields { reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -1506,7 +1572,9 @@ const QUERY_REFERENCE_SYMBOL: unique symbol; interface QueryFunctionOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1536,10 +1604,6 @@ class QueryInfo { }): this; // (undocumented) lastRequestId: number; - // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts - // - // (undocumented) - listeners: Set; // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) @@ -1553,14 +1617,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1582,9 +1642,6 @@ export interface QueryKey { __queryKey?: string; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1627,6 +1684,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1694,8 +1753,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1766,8 +1823,6 @@ export interface QueryReference extends Q toPromise?: unknown; } -// Warning: (ae-forgotten-export) The symbol "PromiseWithState" needs to be exported by the entry point index.d.ts -// // @public (undocumented) type QueryRefPromise = PromiseWithState>>; @@ -1973,15 +2028,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreFunction = ObservableQueryFields["subscribeToMore"]; +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1999,6 +2074,13 @@ class SuspenseCache { constructor(options?: SuspenseCacheOptions); // (undocumented) add(cacheKey: CacheKey, queryRef: InternalQueryReference): void; + // Warning: (ae-forgotten-export) The symbol "FragmentCacheKey" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "FragmentReference" needs to be exported by the entry point index.d.ts + // + // (undocumented) + getFragmentRef(cacheKey: FragmentCacheKey, client: ApolloClient, options: WatchFragmentOptions & { + from: string; + }): FragmentReference; // (undocumented) getQueryRef(cacheKey: CacheKey, createObservable: () => ObservableQuery): InternalQueryReference; } @@ -2098,12 +2180,22 @@ export function unwrapQueryRef(queryRef: Partial>) type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) export function updateWrappedQueryRef(queryRef: WrappedQueryRef, promise: QueryRefPromise): void; @@ -2203,8 +2295,6 @@ function useFragment(options: UseFragme // @public (undocumented) interface UseFragmentOptions extends Omit, NoInfer_2>, "id" | "query" | "optimistic" | "previousResult" | "returnPartialData">, Omit, "id" | "variables" | "returnPartialData"> { client?: ApolloClient; - // Warning: (ae-forgotten-export) The symbol "FragmentType" needs to be exported by the entry point index.d.ts - // // (undocumented) from: StoreObject | Reference | FragmentType> | string | null; // (undocumented) @@ -2251,6 +2341,41 @@ interface UseReadQueryResult { networkStatus: NetworkStatus; } +// Warning: (ae-forgotten-export) The symbol "UseSuspenseFragmentOptions" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "UseSuspenseFragmentResult" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: NonNullable>; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: null; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: From; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; + +// @public (undocumented) +type UseSuspenseFragmentOptions = { + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: From; + optimistic?: boolean; + client?: ApolloClient; +} & VariablesOption>; + +// @public (undocumented) +type UseSuspenseFragmentResult = { + data: MaybeMasked; +}; + // Warning: (ae-forgotten-export) The symbol "SuspenseQueryHookOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "UseSuspenseQueryResult" needs to be exported by the entry point index.d.ts // @@ -2318,7 +2443,7 @@ type VariablesOption = [ TVariables ] extends [never] ? { variables?: Record; -} : {} extends OnlyRequiredProperties ? { +} : Record extends OnlyRequiredProperties ? { variables?: TVariables; } : { variables: TVariables; @@ -2378,6 +2503,10 @@ interface WrappableHooks { // // (undocumented) useReadQuery: typeof useReadQuery; + // Warning: (ae-forgotten-export) The symbol "useSuspenseFragment" needs to be exported by the entry point index.d.ts + // + // (undocumented) + useSuspenseFragment: typeof useSuspenseFragment; // Warning: (ae-forgotten-export) The symbol "useSuspenseQuery" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2406,18 +2535,18 @@ export function wrapQueryRef(inter // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:38:3 - (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:54:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:78:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts -// src/react/query-preloader/createQueryPreloader.ts:145:3 - (ae-forgotten-export) The symbol "PreloadQueryFetchPolicy" needs to be exported by the entry point index.d.ts -// src/react/query-preloader/createQueryPreloader.ts:167:5 - (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts +// src/core/types.ts:172:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:51:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:75:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useSuspenseFragment.ts:70:5 - (ae-forgotten-export) The symbol "From" needs to be exported by the entry point index.d.ts +// src/react/query-preloader/createQueryPreloader.ts:77:5 - (ae-forgotten-export) The symbol "PreloadQueryFetchPolicy" needs to be exported by the entry point index.d.ts +// src/react/query-preloader/createQueryPreloader.ts:117:3 - (ae-forgotten-export) The symbol "RefetchWritePolicy" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-react_ssr.api.md b/.api-reports/api-report-react_ssr.api.md index 9c6b605a377..b4650f350f3 100644 --- a/.api-reports/api-report-react_ssr.api.md +++ b/.api-reports/api-report-react_ssr.api.md @@ -1210,6 +1210,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1229,6 +1231,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1245,8 +1251,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1263,8 +1270,9 @@ interface ObservableQueryFields { reobserve: (newOptions?: Partial>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -1329,7 +1337,9 @@ interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } @@ -1353,10 +1363,6 @@ class QueryInfo { }): this; // (undocumented) lastRequestId: number; - // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts - // - // (undocumented) - listeners: Set; // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) @@ -1370,14 +1376,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1393,9 +1395,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1438,6 +1437,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1505,8 +1506,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1776,12 +1775,35 @@ Item type StoreValue = number | string | string[] | Reference | Reference[] | null | undefined | void | Object; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} + +// @public (undocumented) +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1855,12 +1877,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -1906,13 +1938,13 @@ interface WatchQueryOptions, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1360,6 +1362,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1376,8 +1382,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1436,10 +1443,6 @@ class QueryInfo { }): this; // (undocumented) lastRequestId: number; - // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts - // - // (undocumented) - listeners: Set; // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) @@ -1453,14 +1456,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1476,9 +1475,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1519,6 +1515,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1586,8 +1584,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1824,12 +1820,29 @@ type StoreValue = number | string | string[] | Reference | Reference[] | null | export function subscribeAndCount(reject: (reason: any) => any, observable: Observable, cb: (handleCount: number, result: TResult) => any): Subscription; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1906,12 +1919,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -1974,13 +1997,13 @@ export function withWarningSpy(it: (...args: TArgs // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/types.ts:172:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_core.api.md b/.api-reports/api-report-testing_core.api.md index 82e35138da4..7b577b57b93 100644 --- a/.api-reports/api-report-testing_core.api.md +++ b/.api-reports/api-report-testing_core.api.md @@ -1296,6 +1296,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1315,6 +1317,8 @@ class ObservableQuery>): Subscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1331,8 +1337,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1391,10 +1398,6 @@ class QueryInfo { }): this; // (undocumented) lastRequestId: number; - // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts - // - // (undocumented) - listeners: Set; // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) @@ -1408,14 +1411,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -1431,9 +1430,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -1476,6 +1472,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1543,8 +1541,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -1781,12 +1777,29 @@ type StoreValue = number | string | string[] | Reference | Reference[] | null | export function subscribeAndCount(reject: (reason: any) => any, observable: Observable, cb: (handleCount: number, result: TResult) => any): Subscription; // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -1863,12 +1876,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -1931,13 +1954,13 @@ export function withWarningSpy(it: (...args: TArgs // src/cache/core/types/common.ts:104:3 - (ae-forgotten-export) The symbol "ToReferenceFunction" needs to be exported by the entry point index.d.ts // src/cache/core/types/common.ts:105:3 - (ae-forgotten-export) The symbol "StorageType" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:46:5 - (ae-forgotten-export) The symbol "FragmentMap" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/types.ts:172:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.api-reports/api-report-testing_experimental.api.md b/.api-reports/api-report-testing_experimental.api.md index cfbe5ed404e..101daec293c 100644 --- a/.api-reports/api-report-testing_experimental.api.md +++ b/.api-reports/api-report-testing_experimental.api.md @@ -79,7 +79,7 @@ interface TestSchemaOptions { // 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:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts // src/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.api.md b/.api-reports/api-report-utilities.api.md index c8a866c726d..068367ef9a1 100644 --- a/.api-reports/api-report-utilities.api.md +++ b/.api-reports/api-report-utilities.api.md @@ -715,7 +715,7 @@ export class DeepMerger { // // @public (undocumented) export type DeepOmit = T extends DeepOmitPrimitive ? T : { - [P in Exclude]: T[P] extends infer TP ? TP extends DeepOmitPrimitive ? TP : TP extends any[] ? DeepOmitArray : DeepOmit : never; + [P in keyof T as P extends K ? never : P]: T[P] extends infer TP ? TP extends DeepOmitPrimitive ? TP : TP extends any[] ? DeepOmitArray : DeepOmit : never; }; // @public (undocumented) @@ -1987,6 +1987,8 @@ class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -2004,6 +2006,8 @@ class ObservableQuery>): ObservableSubscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -2020,8 +2026,9 @@ class ObservableQuery(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + // Warning: (ae-forgotten-export) The symbol "UpdateQueryMapFn" needs to be exported by the entry point index.d.ts + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -2166,10 +2173,6 @@ class QueryInfo { }): this; // (undocumented) lastRequestId: number; - // Warning: (ae-forgotten-export) The symbol "QueryListener" needs to be exported by the entry point index.d.ts - // - // (undocumented) - listeners: Set; // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) @@ -2183,14 +2186,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -2206,9 +2205,6 @@ class QueryInfo { variables?: Record; } -// @public (undocumented) -type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -2251,6 +2247,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2318,8 +2316,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2652,12 +2648,29 @@ class Stump extends Layer { } // @public (undocumented) -type SubscribeToMoreOptions = { +interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // Warning: (ae-forgotten-export) The symbol "SubscribeToMoreUpdateQueryFn" needs to be exported by the entry point index.d.ts + // + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -2782,12 +2795,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} + +// @public (undocumented) +type UpdateQueryOptions = { + variables?: TVariables; +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) interface UriFunction { @@ -2876,13 +2899,13 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/writeToStore.ts:65:7 - (ae-forgotten-export) The symbol "MergeTree" needs to be exported by the entry point index.d.ts // src/core/LocalState.ts:71:3 - (ae-forgotten-export) The symbol "ApolloClient" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/types.ts:175:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts -// src/core/types.ts:204:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts +// src/core/types.ts:172:3 - (ae-forgotten-export) The symbol "MutationQueryReducer" needs to be exported by the entry point index.d.ts +// src/core/types.ts:201:5 - (ae-forgotten-export) The symbol "Resolver" needs to be exported by the entry point index.d.ts +// src/core/watchQueryOptions.ts:357:2 - (ae-forgotten-export) The symbol "UpdateQueryOptions" needs to be exported by the entry point index.d.ts // src/utilities/graphql/storeUtils.ts:226:12 - (ae-forgotten-export) The symbol "storeKeyNameStringify" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:76:3 - (ae-forgotten-export) The symbol "TRelayEdge" needs to be exported by the entry point index.d.ts // src/utilities/policies/pagination.ts:77:3 - (ae-forgotten-export) The symbol "TRelayPageInfo" needs to be exported by the entry point index.d.ts diff --git a/.api-reports/api-report.api.md b/.api-reports/api-report.api.md index f17d91511c0..79b576393fb 100644 --- a/.api-reports/api-report.api.md +++ b/.api-reports/api-report.api.md @@ -342,6 +342,7 @@ type BackgroundQueryHookOptionsNoInfer = ApolloCache> extends MutationSharedOptions { client?: ApolloClient; + // @deprecated ignoreResults?: boolean; notifyOnNetworkStatusChange?: boolean; onCompleted?: (data: MaybeMasked, clientOptions?: BaseMutationOptions) => void; @@ -1093,6 +1094,9 @@ TData }; } : never : never; +// @public (undocumented) +type From = StoreObject | Reference | FragmentType> | string | null; + // @public (undocumented) export const from: typeof ApolloLink.from; @@ -1461,7 +1465,9 @@ export interface LazyQueryHookExecOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; } @@ -1905,6 +1911,8 @@ export class ObservableQuery, variables?: TVariables): boolean | undefined; + // @internal (undocumented) + protected notify(): void; // (undocumented) readonly options: WatchQueryOptions; // (undocumented) @@ -1924,6 +1932,8 @@ export class ObservableQuery>): ObservableSubscription; // (undocumented) result(): Promise>>; + // @internal (undocumented) + protected scheduleNotify(): void; // (undocumented) setOptions(newOptions: Partial>): Promise>>; setVariables(variables: TVariables): Promise> | void>; @@ -1939,8 +1951,8 @@ export class ObservableQuery>): void; startPolling(pollInterval: number): void; stopPolling(): void; - subscribeToMore(options: SubscribeToMoreOptions): () => void; - updateQuery(mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked): void; + subscribeToMore(options: SubscribeToMoreOptions): () => void; + updateQuery(mapFn: UpdateQueryMapFn): void; get variables(): TVariables | undefined; } @@ -1957,8 +1969,8 @@ export interface ObservableQueryFields>, newNetworkStatus?: NetworkStatus) => Promise>>; startPolling: (pollInterval: number) => void; stopPolling: () => void; - subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: (mapFn: (previousQueryResult: Unmasked, options: Pick, "variables">) => Unmasked) => void; + subscribeToMore: SubscribeToMoreFunction; + updateQuery: (mapFn: UpdateQueryMapFn) => void; variables: TVariables | undefined; } @@ -2112,8 +2124,6 @@ export interface PreloadQueryFunction { (query: DocumentNode | TypedDocumentNode, ...[options]: PreloadQueryOptionsArg>): PreloadedQueryRef; } -// Warning: (ae-forgotten-export) The symbol "VariablesOption" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export type PreloadQueryOptions = { canonizeResults?: boolean; @@ -2176,7 +2186,9 @@ export interface QueryDataOptions extends BaseQueryOptions { // @internal (undocumented) defaultOptions?: Partial>; + // @deprecated onCompleted?: (data: MaybeMasked) => void; + // @deprecated onError?: (error: ApolloError) => void; skip?: boolean; } @@ -2205,8 +2217,6 @@ class QueryInfo { // (undocumented) lastRequestId: number; // (undocumented) - listeners: Set; - // (undocumented) markError(error: ApolloError): ApolloError; // (undocumented) markReady(): NetworkStatus; @@ -2219,14 +2229,10 @@ class QueryInfo { // (undocumented) networkStatus?: NetworkStatus; // (undocumented) - notify(): void; - // (undocumented) readonly observableQuery: ObservableQuery | null; // (undocumented) readonly queryId: string; // (undocumented) - reset(): void; - // (undocumented) resetDiff(): void; // (undocumented) resetLastWrite(): void; @@ -2248,9 +2254,6 @@ export interface QueryLazyOptions { variables?: TVariables; } -// @public (undocumented) -export type QueryListener = (queryInfo: QueryInfo) => void; - // @public (undocumented) class QueryManager { // Warning: (ae-forgotten-export) The symbol "QueryManagerOptions" needs to be exported by the entry point index.d.ts @@ -2291,6 +2294,8 @@ class QueryManager { getLocalState(): LocalState; // (undocumented) getObservableQueries(include?: InternalRefetchQueriesInclude): Map>; + // (undocumented) + getOrCreateQuery(queryId: string): QueryInfo; // Warning: (ae-forgotten-export) The symbol "QueryStoreValue" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -2355,8 +2360,6 @@ class QueryManager { // (undocumented) resetErrors(queryId: string): void; // (undocumented) - setObservableQuery(observableQuery: ObservableQuery): void; - // (undocumented) readonly ssrMode: boolean; // (undocumented) startGraphQLSubscription(options: SubscriptionOptions): Observable>; @@ -2731,15 +2734,33 @@ class Stump extends Layer { } // @public (undocumented) -type SubscribeToMoreFunction = ObservableQueryFields["subscribeToMore"]; +export interface SubscribeToMoreFunction { + // (undocumented) + (options: SubscribeToMoreOptions): () => void; +} // @public (undocumented) -export type SubscribeToMoreOptions = { +export interface SubscribeToMoreOptions { + // (undocumented) + context?: DefaultContext; + // (undocumented) document: DocumentNode | TypedDocumentNode; - variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + // (undocumented) onError?: (error: Error) => void; - context?: DefaultContext; + // (undocumented) + updateQuery?: SubscribeToMoreUpdateQueryFn; + // (undocumented) + variables?: TSubscriptionVariables; +} + +// @public (undocumented) +export type SubscribeToMoreUpdateQueryFn = { + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions & { + subscriptionData: { + data: Unmasked; + }; + }): Unmasked | void; }; // @public (undocumented) @@ -2886,18 +2907,22 @@ type UnwrapFragmentRefs = true extends IsAny ? TData : TData exten type UpdateQueries = MutationOptions["updateQueries"]; // @public (undocumented) -type UpdateQueryFn = (previousQueryResult: Unmasked, options: { - subscriptionData: { - data: Unmasked; - }; - variables?: TSubscriptionVariables; -}) => Unmasked; +export interface UpdateQueryMapFn { + // (undocumented) + ( + unsafePreviousData: Unmasked, options: UpdateQueryOptions): Unmasked | void; +} // @public (undocumented) -export interface UpdateQueryOptions { - // (undocumented) +export type UpdateQueryOptions = { variables?: TVariables; -} +} & ({ + complete: true; + previousData: Unmasked; +} | { + complete: false; + previousData: DeepPartial> | undefined; +}); // @public (undocumented) export interface UriFunction { @@ -3083,6 +3108,38 @@ export function useSubscription(options: UseSuspenseFragmentOptions & { + from: NonNullable>; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: null; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions & { + from: From; +}): UseSuspenseFragmentResult; + +// @public (undocumented) +export function useSuspenseFragment(options: UseSuspenseFragmentOptions): UseSuspenseFragmentResult; + +// @public (undocumented) +export type UseSuspenseFragmentOptions = { + fragment: DocumentNode | TypedDocumentNode; + fragmentName?: string; + from: From; + optimistic?: boolean; + client?: ApolloClient; +} & VariablesOption>; + +// @public (undocumented) +export type UseSuspenseFragmentResult = { + data: MaybeMasked; +}; + // @public (undocumented) export function useSuspenseQuery, "variables">>(query: DocumentNode | TypedDocumentNode, options?: SuspenseQueryHookOptions, NoInfer_2> & TOptions): UseSuspenseQueryResult | undefined : TData | undefined : TOptions["returnPartialData"] extends true ? TOptions["skip"] extends boolean ? DeepPartial | undefined : DeepPartial : TOptions["skip"] extends boolean ? TData | undefined : TData, TVariables>; @@ -3143,11 +3200,11 @@ export interface UseSuspenseQueryResult = [ +export type VariablesOption = [ TVariables ] extends [never] ? { variables?: Record; -} : {} extends OnlyRequiredProperties ? { +} : Record extends OnlyRequiredProperties ? { variables?: TVariables; } : { variables: TVariables; @@ -3219,16 +3276,15 @@ interface WriteContext extends ReadMergeModifyContext { // src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "KeySpecifier" needs to be exported by the entry point index.d.ts // src/cache/inmemory/policies.ts:162:3 - (ae-forgotten-export) The symbol "KeyArgsFunction" needs to be exported by the entry point index.d.ts // src/cache/inmemory/types.ts:139:3 - (ae-forgotten-export) The symbol "KeyFieldsFunction" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:120:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts -// src/core/ObservableQuery.ts:121:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:128:5 - (ae-forgotten-export) The symbol "QueryManager" needs to be exported by the entry point index.d.ts +// src/core/ObservableQuery.ts:129:5 - (ae-forgotten-export) The symbol "QueryInfo" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:159:5 - (ae-forgotten-export) The symbol "MutationStoreValue" needs to be exported by the entry point index.d.ts // src/core/QueryManager.ts:414:7 - (ae-forgotten-export) The symbol "UpdateQueries" needs to be exported by the entry point index.d.ts -// src/core/watchQueryOptions.ts:277:2 - (ae-forgotten-export) The symbol "UpdateQueryFn" needs to be exported by the entry point index.d.ts // src/link/http/selectHttpOptionsAndBody.ts:128:32 - (ae-forgotten-export) The symbol "HttpQueryOptions" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:38:3 - (ae-forgotten-export) The symbol "SubscribeToMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:54:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts -// src/react/hooks/useBackgroundQuery.ts:78:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:51:3 - (ae-forgotten-export) The symbol "FetchMoreFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useBackgroundQuery.ts:75:4 - (ae-forgotten-export) The symbol "RefetchFunction" needs to be exported by the entry point index.d.ts // src/react/hooks/useLoadableQuery.ts:120:9 - (ae-forgotten-export) The symbol "ResetFunction" needs to be exported by the entry point index.d.ts +// src/react/hooks/useSuspenseFragment.ts:70:5 - (ae-forgotten-export) The symbol "From" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/.changeset/curvy-seahorses-walk.md b/.changeset/curvy-seahorses-walk.md deleted file mode 100644 index 377f9ee5803..00000000000 --- a/.changeset/curvy-seahorses-walk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@apollo/client": patch ---- - -Fix type of `extensions` in `protocolErrors` for `ApolloError` and the `onError` link. According to the [multipart HTTP subscription protocol](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol), fatal tranport errors follow the [GraphQL error format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) which require `extensions` to be a map as its value instead of an array. diff --git a/.circleci/config.yml b/.circleci/config.yml index c8751534afe..02a012a0be2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,8 +1,5 @@ version: 2.1 -orbs: - secops: apollo/circleci-secops-orb@2.0.7 - jobs: # Filesize: # docker: @@ -15,7 +12,7 @@ jobs: Lint: docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.9.0 steps: - checkout - run: npm version @@ -24,7 +21,7 @@ jobs: Formatting: docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.9.0 steps: - checkout - run: npm ci @@ -32,7 +29,7 @@ jobs: Tests: docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.9.0 parameters: project: type: string @@ -53,7 +50,7 @@ jobs: path: reports/junit Attest: docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.9.0 steps: - checkout - run: npm ci @@ -61,7 +58,7 @@ jobs: BuildTarball: docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.9.0 steps: - checkout - run: npm run ci:precheck @@ -80,7 +77,7 @@ jobs: react: type: string docker: - - image: cimg/node:23.6.0-browsers + - image: cimg/node:23.9.0-browsers steps: - checkout - attach_workspace: @@ -118,7 +115,7 @@ jobs: externalPackage: type: string docker: - - image: cimg/node:23.6.0 + - image: cimg/node:23.9.0 steps: - checkout - attach_workspace: @@ -183,17 +180,3 @@ workflows: - "@types/react@18 @types/react-dom@18" - "@types/react@19 @types/react-dom@19" - "typescript@next" - security-scans: - jobs: - - secops/gitleaks: - context: - - platform-docker-ro - - github-orb - - secops-oidc - git-base-revision: <<#pipeline.git.base_revision>><><> - git-revision: << pipeline.git.revision >> - - secops/semgrep: - context: - - secops-oidc - - github-orb - git-base-revision: <<#pipeline.git.base_revision>><><> diff --git a/.github/workflows/scheduled-test-canary.yml b/.github/workflows/scheduled-test-canary.yml index 42d5442d948..f94dc74be0b 100644 --- a/.github/workflows/scheduled-test-canary.yml +++ b/.github/workflows/scheduled-test-canary.yml @@ -28,7 +28,7 @@ jobs: ref: ${{ matrix.branch }} - uses: actions/setup-node@v4 with: - node-version: 22.x + node-version: ">=23.6.0" - uses: bahmutov/npm-install@v1 - run: | npm install react@${{ matrix.tag }} react-dom@${{ matrix.tag }} diff --git a/.size-limit.cjs b/.size-limit.cjs index a91cfa60a04..23a23080afe 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -1,5 +1,33 @@ const limits = require("./.size-limits.json"); +/* prettier-ignore */ +const nameMapping = { + 'import { ApolloClient, InMemoryCache, HttpLink } from "dist/main.cjs"': 'import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (CJS)', + 'import { ApolloClient, InMemoryCache, HttpLink } from "dist/main.cjs" (production)': 'import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (production) (CJS)', + 'import { ApolloClient, InMemoryCache, HttpLink } from "dist/index.js"': 'import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client"', + 'import { ApolloClient, InMemoryCache, HttpLink } from "dist/index.js" (production)': 'import { ApolloClient, InMemoryCache, HttpLink } from "@apollo/client" (production)', + 'import { ApolloProvider } from "dist/react/index.js"': 'import { ApolloProvider } from "@apollo/client/react"', + 'import { ApolloProvider } from "dist/react/index.js" (production)': 'import { ApolloProvider } from "@apollo/client/react" (production)', + 'import { useQuery } from "dist/react/index.js"': 'import { useQuery } from "@apollo/client/react"', + 'import { useQuery } from "dist/react/index.js" (production)': 'import { useQuery } from "@apollo/client/react" (production)', + 'import { useLazyQuery } from "dist/react/index.js"': 'import { useLazyQuery } from "@apollo/client/react"', + 'import { useLazyQuery } from "dist/react/index.js" (production)': 'import { useLazyQuery } from "@apollo/client/react"', + 'import { useMutation } from "dist/react/index.js"': 'import { useMutation } from "@apollo/client/react"', + 'import { useMutation } from "dist/react/index.js" (production)': 'import { useMutation } from "@apollo/client/react" (production)', + 'import { useSubscription } from "dist/react/index.js"': 'import { useSubscription } from "@apollo/client/react"', + 'import { useSubscription } from "dist/react/index.js" (production)': 'import { useSubscription } from "@apollo/client/react" (production)', + 'import { useSuspenseQuery } from "dist/react/index.js"': 'import { useSuspenseQuery } from "@apollo/client/react"', + 'import { useSuspenseQuery } from "dist/react/index.js" (production)': 'import { useSuspenseQuery } from "@apollo/client/react" (production)', + 'import { useBackgroundQuery } from "dist/react/index.js"': 'import { useBackgroundQuery } from "@apollo/client/react"', + 'import { useBackgroundQuery } from "dist/react/index.js" (production)': 'import { useBackgroundQuery } from "@apollo/client/react" (production)', + 'import { useLoadableQuery } from "dist/react/index.js"': 'import { useLoadableQuery } from "@apollo/client/react"', + 'import { useLoadableQuery } from "dist/react/index.js" (production)': 'import { useLoadableQuery } from "@apollo/client/react" (production)', + 'import { useReadQuery } from "dist/react/index.js"': 'import { useReadQuery } from "@apollo/client/react"', + 'import { useReadQuery } from "dist/react/index.js" (production)': 'import { useReadQuery } from "@apollo/client/react" (production)', + 'import { useFragment } from "dist/react/index.js"': 'import { useFragment } from "@apollo/client/react"', + 'import { useFragment } from "dist/react/index.js" (production)': 'import { useFragment } from "@apollo/client/react" (production)', +}; + const checks = [ { path: "dist/apollo-client.min.cjs", @@ -73,6 +101,7 @@ const checks = [ ] ) .map((value) => { + value.name = nameMapping[value.name] || value.name; value.limit = limits[value.name]; return value; }); diff --git a/.size-limits.json b/.size-limits.json index f7f8f2606dc..9616d91d1cf 100644 --- a/.size-limits.json +++ b/.size-limits.json @@ -1,4 +1,4 @@ { - "dist/apollo-client.min.cjs": 41642, - "import { ApolloClient, InMemoryCache, HttpLink } from \"dist/index.js\" (production)": 34384 + "dist/apollo-client.min.cjs": 42332, + "import { ApolloClient, InMemoryCache, HttpLink } from \"@apollo/client\" (production)": 34542 } diff --git a/.vscode/settings.json b/.vscode/settings.json index c2580b79482..604746d4cfa 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,6 +5,9 @@ "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "typescript.tsdk": "node_modules/typescript/lib", + "editor.codeActionsOnSave": { + "source.organizeImports": "never" + }, "cSpell.enableFiletypes": ["mdx"], "jest.jestCommandLine": "node --expose-gc node_modules/.bin/jest --config ./config/jest.config.js --ignoreProjects 'ReactDOM 17' --runInBand" } diff --git a/CHANGELOG.md b/CHANGELOG.md index fb62dad7b77..3f9ff21d53b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,228 @@ # @apollo/client +## 3.13.8 + +### Patch Changes + +- [#12567](https://github.com/apollographql/apollo-client/pull/12567) [`c19d415`](https://github.com/apollographql/apollo-client/commit/c19d41513cac0cc143aa7358f26c89c9408da102) Thanks [@thearchitector](https://github.com/thearchitector)! - Fix in-flight multipart urql subscription cancellation + +## 3.13.7 + +### Patch Changes + +- [#12540](https://github.com/apollographql/apollo-client/pull/12540) [`0098932`](https://github.com/apollographql/apollo-client/commit/009893220934081f6e5733bff5863c768a597117) Thanks [@phryneas](https://github.com/phryneas)! - Refactor: Move notification scheduling logic from `QueryInfo` to `ObservableQuery` + +- [#12540](https://github.com/apollographql/apollo-client/pull/12540) [`0098932`](https://github.com/apollographql/apollo-client/commit/009893220934081f6e5733bff5863c768a597117) Thanks [@phryneas](https://github.com/phryneas)! - Refactored cache emit logic for ObservableQuery. This should be an invisible change. + +## 3.13.6 + +### Patch Changes + +- [#12285](https://github.com/apollographql/apollo-client/pull/12285) [`cdc55ff`](https://github.com/apollographql/apollo-client/commit/cdc55ff54bf4c83ec8571508ec4bf8156af1bc97) Thanks [@phryneas](https://github.com/phryneas)! - keep ObservableQuery created by useQuery non-active before it is first subscribed + +## 3.13.5 + +### Patch Changes + +- [#12461](https://github.com/apollographql/apollo-client/pull/12461) [`12c8d06`](https://github.com/apollographql/apollo-client/commit/12c8d06f1ef7cfbece8e3a63b7ad09d91334f663) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix an issue where a `cache-first` query would return the result for previous variables when a cache update is issued after simultaneously changing variables and skipping the query. + +## 3.13.4 + +### Patch Changes + +- [#12420](https://github.com/apollographql/apollo-client/pull/12420) [`fee9368`](https://github.com/apollographql/apollo-client/commit/fee9368750e242ea03dea8d1557683506d411d8d) Thanks [@jorenbroekema](https://github.com/jorenbroekema)! - Use import star from `rehackt` to prevent issues with importing named exports from external CJS modules. + +## 3.13.3 + +### Patch Changes + +- [#12362](https://github.com/apollographql/apollo-client/pull/12362) [`f6d387c`](https://github.com/apollographql/apollo-client/commit/f6d387c166cc76f08135966fb6d74fd8fe808c21) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fixes an issue where calling `observableQuery.getCurrentResult()` when the `errorPolicy` was set to `all` would return the `networkStatus` as `NetworkStatus.ready` when there were errors returned in the result. This has been corrected to report `NetworkStatus.error`. + + This bug also affected the `useQuery` and `useLazyQuery` hooks and may affect you if you check for `networkStatus` in your component. + +## 3.13.2 + +### Patch Changes + +- [#12409](https://github.com/apollographql/apollo-client/pull/12409) [`6aa2f3e`](https://github.com/apollographql/apollo-client/commit/6aa2f3e81ee0ae59da7ae0b12000bb5a55ec5c6d) Thanks [@phryneas](https://github.com/phryneas)! - To mitigate problems when Apollo Client ends up more than once in the bundle, some unique symbols were converted into `Symbol.for` calls. + +- [#12392](https://github.com/apollographql/apollo-client/pull/12392) [`644bb26`](https://github.com/apollographql/apollo-client/commit/644bb2662168a9bac0519be6979f0db38b0febc4) Thanks [@Joja81](https://github.com/Joja81)! - Fixes an issue where the DeepOmit type would turn optional properties into required properties. This should only affect you if you were using the omitDeep or stripTypename utilities exported by Apollo Client. + +- [#12404](https://github.com/apollographql/apollo-client/pull/12404) [`4332b88`](https://github.com/apollographql/apollo-client/commit/4332b886f0409145af57f26d334f86e5a1b465c5) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Show `NaN` rather than converting to `null` in debug messages from `MockLink` for unmatched `variables` values. + +## 3.13.1 + +### Patch Changes + +- [#12369](https://github.com/apollographql/apollo-client/pull/12369) [`bdfc5b2`](https://github.com/apollographql/apollo-client/commit/bdfc5b2e386ed5f835716a542de0cf17da37f7fc) Thanks [@phryneas](https://github.com/phryneas)! - `ObervableQuery.refetch`: don't refetch with `cache-and-network`, swich to `network-only` instead + +- [#12375](https://github.com/apollographql/apollo-client/pull/12375) [`d3f8f13`](https://github.com/apollographql/apollo-client/commit/d3f8f130718ef50531ca0079192c2672a513814a) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Export the `UseSuspenseFragmentOptions` type. + +## 3.13.0 + +### Minor Changes + +- [#12066](https://github.com/apollographql/apollo-client/pull/12066) [`c01da5d`](https://github.com/apollographql/apollo-client/commit/c01da5da639d4d9e882d380573b7876df4a1d65b) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds a new `useSuspenseFragment` hook. + + `useSuspenseFragment` suspends until `data` is complete. It is a drop-in replacement for `useFragment` when you prefer to use Suspense to control the loading state of a fragment. See the [documentation](https://www.apollographql.com/docs/react/data/fragments#usesuspensefragment) for more details. + +- [#12174](https://github.com/apollographql/apollo-client/pull/12174) [`ba5cc33`](https://github.com/apollographql/apollo-client/commit/ba5cc330f8734a989eef71e883861f848388ac0c) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Ensure errors thrown in the `onCompleted` callback from `useMutation` don't call `onError`. + +- [#12340](https://github.com/apollographql/apollo-client/pull/12340) [`716d02e`](https://github.com/apollographql/apollo-client/commit/716d02ec9c5b1448f50cb50a0306a345310a2342) Thanks [@phryneas](https://github.com/phryneas)! - Deprecate the `onCompleted` and `onError` callbacks of `useQuery` and `useLazyQuery`. + For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Provide a more type-safe option for the previous data value passed to `observableQuery.updateQuery`. Using it could result in crashes at runtime as this callback could be called with partial data even though its type reported the value as a complete result. + + The `updateQuery` callback function is now called with a new type-safe `previousData` property and a new `complete` property in the 2nd argument that determines whether `previousData` is a complete or partial result. + + As a result of this change, it is recommended to use the `previousData` property passed to the 2nd argument of the callback rather than using the previous data value from the first argument since that value is not type-safe. The first argument is now deprecated and will be removed in a future version of Apollo Client. + + ```ts + observableQuery.updateQuery( + (unsafePreviousData, { previousData, complete }) => { + previousData; + // ^? TData | DeepPartial | undefined + + if (complete) { + previousData; + // ^? TData + } else { + previousData; + // ^? DeepPartial | undefined + } + } + ); + ``` + +- [#12174](https://github.com/apollographql/apollo-client/pull/12174) [`ba5cc33`](https://github.com/apollographql/apollo-client/commit/ba5cc330f8734a989eef71e883861f848388ac0c) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Reject the mutation promise if errors are thrown in the `onCompleted` callback of `useMutation`. + +### Patch Changes + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Fix the return type of the `updateQuery` function to allow for `undefined`. `updateQuery` had the ability to bail out of the update by returning a falsey value, but the return type enforced a query value. + + ```ts + observableQuery.updateQuery( + (unsafePreviousData, { previousData, complete }) => { + if (!complete) { + // Bail out of the update by returning early + return; + } + + // ... + } + ); + ``` + +- [#12296](https://github.com/apollographql/apollo-client/pull/12296) [`2422df2`](https://github.com/apollographql/apollo-client/commit/2422df202a7ec71365d5a8ab5b3b554fcf60e4af) Thanks [@Cellule](https://github.com/Cellule)! - Deprecate option `ignoreResults` in `useMutation`. + Once this option is removed, existing code still using it might see increase in re-renders. + If you don't want to synchronize your component state with the mutation, please use `useApolloClient` to get your ApolloClient instance and call `client.mutate` directly. + +- [#12338](https://github.com/apollographql/apollo-client/pull/12338) [`67c16c9`](https://github.com/apollographql/apollo-client/commit/67c16c93897e36be980ba2139ee8bd3f24ab8558) Thanks [@phryneas](https://github.com/phryneas)! - In case of a multipart response (e.g. with `@defer`), query deduplication will + now keep going until the final chunk has been received. + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Fix the type of the `variables` property passed as the 2nd argument to the `subscribeToMore` callback. This was previously reported as the `variables` type for the subscription itself, but is now properly typed as the query `variables`. + +## 3.13.0-rc.0 + +### Minor Changes + +- [#12066](https://github.com/apollographql/apollo-client/pull/12066) [`c01da5d`](https://github.com/apollographql/apollo-client/commit/c01da5da639d4d9e882d380573b7876df4a1d65b) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Adds a new `useSuspenseFragment` hook. + + `useSuspenseFragment` suspends until `data` is complete. It is a drop-in replacement for `useFragment` when you prefer to use Suspense to control the loading state of a fragment. + +- [#12174](https://github.com/apollographql/apollo-client/pull/12174) [`ba5cc33`](https://github.com/apollographql/apollo-client/commit/ba5cc330f8734a989eef71e883861f848388ac0c) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Ensure errors thrown in the `onCompleted` callback from `useMutation` don't call `onError`. + +- [#12340](https://github.com/apollographql/apollo-client/pull/12340) [`716d02e`](https://github.com/apollographql/apollo-client/commit/716d02ec9c5b1448f50cb50a0306a345310a2342) Thanks [@phryneas](https://github.com/phryneas)! - Deprecate the `onCompleted` and `onError` callbacks of `useQuery` and `useLazyQuery`. + For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Provide a more type-safe option for the previous data value passed to `observableQuery.updateQuery`. Using it could result in crashes at runtime as this callback could be called with partial data even though its type reported the value as a complete result. + + The `updateQuery` callback function is now called with a new type-safe `previousData` property and a new `complete` property in the 2nd argument that determines whether `previousData` is a complete or partial result. + + As a result of this change, it is recommended to use the `previousData` property passed to the 2nd argument of the callback rather than using the previous data value from the first argument since that value is not type-safe. The first argument is now deprecated and will be removed in a future version of Apollo Client. + + ```ts + observableQuery.updateQuery( + (unsafePreviousData, { previousData, complete }) => { + previousData; + // ^? TData | DeepPartial | undefined + + if (complete) { + previousData; + // ^? TData + } else { + previousData; + // ^? DeepPartial | undefined + } + } + ); + ``` + +- [#12174](https://github.com/apollographql/apollo-client/pull/12174) [`ba5cc33`](https://github.com/apollographql/apollo-client/commit/ba5cc330f8734a989eef71e883861f848388ac0c) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Reject the mutation promise if errors are thrown in the `onCompleted` callback of `useMutation`. + +### Patch Changes + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Fix the return type of the `updateQuery` function to allow for `undefined`. `updateQuery` had the ability to bail out of the update by returning a falsey value, but the return type enforced a query value. + + ```ts + observableQuery.updateQuery( + (unsafePreviousData, { previousData, complete }) => { + if (!complete) { + // Bail out of the update by returning early + return; + } + + // ... + } + ); + ``` + +- [#12296](https://github.com/apollographql/apollo-client/pull/12296) [`2422df2`](https://github.com/apollographql/apollo-client/commit/2422df202a7ec71365d5a8ab5b3b554fcf60e4af) Thanks [@Cellule](https://github.com/Cellule)! - Deprecate option `ignoreResults` in `useMutation`. + Once this option is removed, existing code still using it might see increase in re-renders. + If you don't want to synchronize your component state with the mutation, please use `useApolloClient` to get your ApolloClient instance and call `client.mutate` directly. + +- [#12338](https://github.com/apollographql/apollo-client/pull/12338) [`67c16c9`](https://github.com/apollographql/apollo-client/commit/67c16c93897e36be980ba2139ee8bd3f24ab8558) Thanks [@phryneas](https://github.com/phryneas)! - In case of a multipart response (e.g. with `@defer`), query deduplication will + now keep going until the final chunk has been received. + +- [#12276](https://github.com/apollographql/apollo-client/pull/12276) [`670f112`](https://github.com/apollographql/apollo-client/commit/670f112a7d9d85cb357eb279a488ac2c6d0137a9) Thanks [@Cellule](https://github.com/Cellule)! - Fix the type of the `variables` property passed as the 2nd argument to the `subscribeToMore` `updateQuery` callback. This was previously reported as the `variables` type for the subscription itself, but is now properly typed as the query `variables`. + +## 3.12.11 + +### Patch Changes + +- [#12351](https://github.com/apollographql/apollo-client/pull/12351) [`3da908b`](https://github.com/apollographql/apollo-client/commit/3da908b1dde73847805a41c287a83700b2b88887) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fixes an issue where the wrong `networkStatus` and `loading` value was emitted from `observableQuery` when calling `fetchMore` with a `no-cache` fetch policy. The `networkStatus` now properly reports as `ready` and `loading` as `false` after the result is returned. + +- [#12354](https://github.com/apollographql/apollo-client/pull/12354) [`a24ef94`](https://github.com/apollographql/apollo-client/commit/a24ef9474f8f7a864f8b866563f8f7e661d2533f) Thanks [@phryneas](https://github.com/phryneas)! - Fix missing `main.d.cts` file + +## 3.12.10 + +### Patch Changes + +- [#12341](https://github.com/apollographql/apollo-client/pull/12341) [`f2bb0b9`](https://github.com/apollographql/apollo-client/commit/f2bb0b9955564e432345ee8bd431290e698d092c) Thanks [@phryneas](https://github.com/phryneas)! - `useReadQuery`/`useQueryRefHandlers`: Fix a "hook order" warning that might be emitted in React 19 dev mode. + +- [#12342](https://github.com/apollographql/apollo-client/pull/12342) [`219b26b`](https://github.com/apollographql/apollo-client/commit/219b26ba5a697981ad700e05b926d42db0fb8e59) Thanks [@phryneas](https://github.com/phryneas)! - Add `graphql-ws` `^6.0.3` as a valid `peerDependency` + +## 3.12.9 + +### Patch Changes + +- [#12321](https://github.com/apollographql/apollo-client/pull/12321) [`daa4f33`](https://github.com/apollographql/apollo-client/commit/daa4f3303cfb81e8dca66c21ce3f3dc24946cafb) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Fix type of `extensions` in `protocolErrors` for `ApolloError` and the `onError` link. According to the [multipart HTTP subscription protocol](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol), fatal tranport errors follow the [GraphQL error format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) which require `extensions` to be a map as its value instead of an array. + +- [#12318](https://github.com/apollographql/apollo-client/pull/12318) [`b17968b`](https://github.com/apollographql/apollo-client/commit/b17968b61f0e35b1ba20d081dacee66af8225491) Thanks [@jerelmiller](https://github.com/jerelmiller)! - Allow `RetryLink` to retry an operation when fatal [transport-level errors](https://www.apollographql.com/docs/graphos/routing/operations/subscriptions/multipart-protocol#message-and-error-format) are emitted from multipart subscriptions. + + ```js + const retryLink = new RetryLink({ + attempts: (count, operation, error) => { + if (error instanceof ApolloError) { + // errors available on the `protocolErrors` field in `ApolloError` + console.log(error.protocolErrors); + } + + return true; + }, + }); + ``` + ## 3.12.8 ### Patch Changes diff --git a/README.md b/README.md index 404b83bc620..abf10b2c149 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ --- **Announcement:** -Join 1000+ engineers at GraphQL Summit for talks, workshops, and office hours, Oct 8-10 in NYC. [Get your pass here ->](https://summit.graphql.com/?utm_campaign=github_federation_readme) +Join 1000+ engineers at GraphQL Summit 2025 by Apollo for talks, workshops, and office hours. Oct 6-8, 2025 in San Francisco. [Get your pass here ->](https://www.apollographql.com/graphql-summit-2025?utm_campaign=2025-03-04_graphql-summit-github-announcement&utm_medium=github&utm_source=apollo-server) --- diff --git a/ROADMAP.md b/ROADMAP.md index 6fcfda717aa..c2b59ee316d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # 🔮 Apollo Client Ecosystem Roadmap -**Last updated: 2025-01-22** +**Last updated: 2025-05-07** For up to date release notes, refer to the project's [Changelog](https://github.com/apollographql/apollo-client/blob/main/CHANGELOG.md). @@ -17,18 +17,13 @@ For up to date release notes, refer to the project's [Changelog](https://github. ### Apollo Client -#### 3.13.0 - February 3, 2024 -_Release candidate - January 27th_ +#### 3.14.0 - June 5th, 2025 +_Release candidate - May 29th, 2025 -- `useSuspenseFragment` +- 4.0 compatibility release/deprecations -#### [4.0.0](https://github.com/apollographql/apollo-client/milestone/31) - TBD -_Release candidate - TBD_ - -#### Upcoming features - -##### 3.x.x -- Deprecations and preparations for 4.0 +#### [4.0.0](https://github.com/apollographql/apollo-client/milestone/31) - June 10th, 2025 +_Release candidate - May 29th 2025_ ### GraphQL Testing Library @@ -54,6 +49,9 @@ _These changes will take longer than anticipated due to prioritization on Apollo ### Apollo Client React Framework Integrations - New/more robust documentation -- Support for `@defer` with `PreloadQuery` -- Support for Apollo Client Streaming in TanStack Router -- Support for Apollo Client Streaming in React Router 7 (merged) + +**TanStack Start** +- Support for Apollo Client Streaming in TanStack Router - will stay alpha + +**React Router** +- Support for Apollo Client Streaming in React Router 7 - will stay alpha diff --git a/config/jest.config.js b/config/jest.config.js index 977c2e8e80a..011836cc41f 100644 --- a/config/jest.config.js +++ b/config/jest.config.js @@ -44,6 +44,7 @@ const react17TestFileIgnoreList = [ // 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__/useSuspenseFragment.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/config/prepareDist.js b/config/prepareDist.js index 68bfbce2c2e..e96c108840c 100644 --- a/config/prepareDist.js +++ b/config/prepareDist.js @@ -97,7 +97,6 @@ entryPoints.forEach(function buildCts({ dirs, bundleName = dirs[dirs.length - 1], }) { - if (!dirs.length) return; fs.writeFileSync( path.join(distRoot, ...dirs, `${bundleName}.d.cts`), 'export * from "./index.d.ts";\n' diff --git a/docs/source/_sidebar.yaml b/docs/source/_sidebar.yaml index d26de2ac14a..b30a00a5575 100644 --- a/docs/source/_sidebar.yaml +++ b/docs/source/_sidebar.yaml @@ -35,6 +35,8 @@ items: href: ./data/error-handling - label: Document transforms href: ./data/document-transforms + - label: Deferring response data + href: ./data/defer - label: Best practices href: ./data/operation-best-practices - label: Caching diff --git a/docs/source/api/link/apollo-link-context.md b/docs/source/api/link/apollo-link-context.mdx similarity index 100% rename from docs/source/api/link/apollo-link-context.md rename to docs/source/api/link/apollo-link-context.mdx diff --git a/docs/source/api/link/apollo-link-error.md b/docs/source/api/link/apollo-link-error.mdx similarity index 100% rename from docs/source/api/link/apollo-link-error.md rename to docs/source/api/link/apollo-link-error.mdx diff --git a/docs/source/api/link/apollo-link-http.md b/docs/source/api/link/apollo-link-http.mdx similarity index 100% rename from docs/source/api/link/apollo-link-http.md rename to docs/source/api/link/apollo-link-http.mdx diff --git a/docs/source/api/link/apollo-link-rest.md b/docs/source/api/link/apollo-link-rest.mdx similarity index 100% rename from docs/source/api/link/apollo-link-rest.md rename to docs/source/api/link/apollo-link-rest.mdx diff --git a/docs/source/api/link/apollo-link-retry.md b/docs/source/api/link/apollo-link-retry.mdx similarity index 100% rename from docs/source/api/link/apollo-link-retry.md rename to docs/source/api/link/apollo-link-retry.mdx diff --git a/docs/source/api/link/apollo-link-schema.md b/docs/source/api/link/apollo-link-schema.mdx similarity index 100% rename from docs/source/api/link/apollo-link-schema.md rename to docs/source/api/link/apollo-link-schema.mdx diff --git a/docs/source/api/link/apollo-link-subscriptions.md b/docs/source/api/link/apollo-link-subscriptions.mdx similarity index 100% rename from docs/source/api/link/apollo-link-subscriptions.md rename to docs/source/api/link/apollo-link-subscriptions.mdx diff --git a/docs/source/api/link/apollo-link-ws.md b/docs/source/api/link/apollo-link-ws.mdx similarity index 100% rename from docs/source/api/link/apollo-link-ws.md rename to docs/source/api/link/apollo-link-ws.mdx diff --git a/docs/source/api/link/community-links.md b/docs/source/api/link/community-links.mdx similarity index 100% rename from docs/source/api/link/community-links.md rename to docs/source/api/link/community-links.mdx diff --git a/docs/source/api/react/ssr.md b/docs/source/api/react/ssr.mdx similarity index 87% rename from docs/source/api/react/ssr.md rename to docs/source/api/react/ssr.mdx index 661566b443f..dfa7a60e2a4 100644 --- a/docs/source/api/react/ssr.md +++ b/docs/source/api/react/ssr.mdx @@ -20,7 +20,7 @@ import { getDataFromTree } from "@apollo/client/react/ssr"; | Param | Type | description | | - | - | - | | `tree` | React.ReactNode | The React tree you would like to render and fetch data for. | -| `context` | { [key: string]: any } | Optional values you would like to make available in the React Context during rendering / data retrieval. | +| `context` | `{ [key: string]: any }` | Optional values you would like to make available in the React Context during rendering / data retrieval. | ### Result @@ -42,7 +42,7 @@ import { renderToStringWithData } from "@apollo/client/react/ssr"; | Param | Type | description | | - | - | - | -| `component` | ReactElement | The React component tree you would like to render and fetch data for. | +| `component` | `ReactElement` | The React component tree you would like to render and fetch data for. | ### Result diff --git a/docs/source/api/react/testing.md b/docs/source/api/react/testing.mdx similarity index 100% rename from docs/source/api/react/testing.md rename to docs/source/api/react/testing.mdx diff --git a/docs/source/caching/cache-configuration.mdx b/docs/source/caching/cache-configuration.mdx index 808faefbc3f..209ee073bf6 100644 --- a/docs/source/caching/cache-configuration.mdx +++ b/docs/source/caching/cache-configuration.mdx @@ -180,6 +180,22 @@ const cache = new InMemoryCache({ // array for their keyFields. keyFields: [], }, + Store: { + // If you need to disable normalization, set the keyFields to false + // and the object will be embedded in the parent + keyFields: false + }, + Location: { + // You can also use a function to determine any of the values above. + // The first argument is the reference to the record to be written, and the second is the runtime context + keyFields: (location, context) => { + if (context.readField("state")) { + return ["city", "state", "country"] + } else { + return ["city", "country"] + } + } + } }, }); ``` @@ -195,6 +211,9 @@ This example shows a variety of `typePolicies` with different `keyFields`: Book:{"title":"Fahrenheit 451","author":{"name":"Ray Bradbury"}} ``` * The `AllProducts` type illustrates a special strategy for a **singleton** type. If the cache will only ever contain one `AllProducts` object and that object has _no_ identifying fields, you can provide an empty array for its `keyFields`. +* The `Store` type provides an example of how you can [disable normalization](https://www.apollographql.com/docs/react/caching/cache-configuration/#disabling-normalization) by setting the keyFields to `false` +* The `Location` type provides an example of using custom fuction given object and runtime context to calculate what fields should be used for the key field selection. + * Keep in mind that the first argument here is a reference to the record that will be written. As such, it does NOT include subselected fields, only scalar fields, and it contains aliases from the operation. If you need to read the real type you can use `context.storeObject`. To read even more indepth about how this function can work the best source will be our own [test cases for the cache policies](https://github.com/apollographql/apollo-client/blob/8bc7d4d406402962bf5151cdd2c5c75c9398d10c/src/cache/inmemory/__tests__/policies.ts#L5543-L5622) If an object has multiple `keyFields`, the cache ID always lists those fields in the same order to ensure uniqueness. diff --git a/docs/source/data/defer.mdx b/docs/source/data/defer.mdx index 6b43736590b..eb364a450a5 100644 --- a/docs/source/data/defer.mdx +++ b/docs/source/data/defer.mdx @@ -1,6 +1,7 @@ --- title: "Using the @defer directive in Apollo Client" description: Receive query response data incrementally +minVersion: 3.7.0 --- > **The `@defer` directive is currently at the [General Availability stage](/resources/product-launch-stages/#general-availability) in Apollo Client, and is available by installing `@apollo/client@latest`.** If you have feedback on it, please let us know via [GitHub issues](https://github.com/apollographql/apollo-client/issues/new?assignees=&labels=&template=bug.md). diff --git a/docs/source/data/file-uploads.md b/docs/source/data/file-uploads.mdx similarity index 100% rename from docs/source/data/file-uploads.md rename to docs/source/data/file-uploads.mdx diff --git a/docs/source/data/fragments.mdx b/docs/source/data/fragments.mdx index d8f4fdf4373..4124d0eb53a 100644 --- a/docs/source/data/fragments.mdx +++ b/docs/source/data/fragments.mdx @@ -561,7 +561,7 @@ function Item(props: { item: { __typename: 'Item', id: number }}) { const { complete, data } = useFragment({ fragment: ItemFragment, fragmentName: "ItemFragment", - from: item + from: props.item }); return
  • {complete ? data.text : "incomplete"}
  • ; @@ -573,7 +573,7 @@ function Item(props) { const { complete, data } = useFragment({ fragment: ITEM_FRAGMENT, fragmentName: "ItemFragment", - from: item + from: props.item }); return
  • {complete ? data.text : "incomplete"}
  • ; @@ -589,6 +589,71 @@ function Item(props) { See the [API reference](../api/react/hooks#usefragment) for more details on the supported options. + +## `useSuspenseFragment` + + +For those that have integrated with React [Suspense](https://react.dev/reference/react/Suspense), `useSuspenseFragment` is available as a drop-in replacement for `useFragment`. `useSuspenseFragment` works identically to `useFragment` but will suspend while `data` is incomplete. + +Let's update the example from the previous section to use `useSuspenseFragment`. First, we'll update our `Item` component and replace `useFragment` with `useSuspenseFragment`. Since we are using Suspense, we no longer have to check for a `complete` property to determine if the result is complete because the component will suspend otherwise. + +```tsx +import { useSuspenseFragment } from "@apollo/client"; + +function Item(props) { + const { data } = useSuspenseFragment({ + fragment: ITEM_FRAGMENT, + fragmentName: "ItemFragment", + from: props.item + }); + + return
  • {data.text}
  • ; +} +``` + +Next, we'll will wrap our `Item` components in a `Suspense` boundary to show a loading indicator if the data from `ItemFragment` is not complete. Since we're using Suspense, we'll replace `useQuery` with `useSuspenseQuery` as well: + +```tsx +function List() { + const { data } = useSuspenseQuery(listQuery); + + return ( +
      + {data.list.map(item => ( + }> + + + ))} +
    + ); +} +``` + +And that's it! Suspense made our `Item` component a bit more succinct since we no longer need to check the `complete` property to determine if we can safely use `data`. + + +In most cases, `useSuspenseFragment` will not suspend when rendered as a child of a query component. In this example `useSuspenseQuery` loads the full query data before each `Item` is rendered so the `data` inside each fragment is already complete. The `Suspense` boundary in this example ensures that a loading spinner is shown if field data is removed for any given item in the list in the cache, such as when a manual cache update is performed. + + +### Using `useSuspenseFragment` with `@defer` + +`useSuspenseFragment` is helpful when combined with the [`@defer` directive](./directives#defer) to show a loading state while the fragment data is streamed to the query. Let's update our `GetItemList` query to defer loading the `ItemFragment`'s fields. + +```graphql +query GetItemList { + list { + id + ...ItemFragment @defer + } +} +``` + +Our list will now render as soon as our list returns but before the data for `ItemFragment` is loaded. + + +You **must** ensure that any key fields used to identify the object passed to the `from` option are not deferred. If they are, you risk suspending the `useSuspenseFragment` hook forever. If you need to defer loading key fields, conditionally render the component until the object passed to the `from` option is identifiable by the cache. + + ## Data masking diff --git a/docs/source/data/subscriptions.mdx b/docs/source/data/subscriptions.mdx index 3f3fed9fcac..2f53a8caa63 100644 --- a/docs/source/data/subscriptions.mdx +++ b/docs/source/data/subscriptions.mdx @@ -460,69 +460,75 @@ const COMMENTS_SUBSCRIPTION = gql` -Next, we modify our `CommentsPageWithData` function to add a `subscribeToNewComments` property to the `CommentsPage` component it returns. This property is a function that will be responsible for calling `subscribeToMore` after the component mounts. +Next, we modify our `CommentsPageWithData` component to call `subscribeToMore` after the comments query loads. ```tsx {10-25} function CommentsPageWithData({ params }: CommentsPageWithDataProps) { - const { subscribeToMore, ...result } = useQuery( - COMMENTS_QUERY, - { variables: { postID: params.postID } } - ); + const { subscribeToMore, ...result } = useQuery(COMMENTS_QUERY, { + variables: { postID: params.postID }, + }); + + useEffect(() => { + // This assumes you want to wait to start the subscription + // after the query has loaded. + if (result.data) { + const unsubscribe = subscribeToMore({ + document: COMMENTS_SUBSCRIPTION, + variables: { postID: params.postID }, + updateQuery: (prev, { subscriptionData }) => { + if (!subscriptionData.data) return prev; + const newFeedItem = subscriptionData.data.commentAdded; + + return Object.assign({}, prev, { + post: { + comments: [newFeedItem, ...prev.post.comments], + }, + }); + }, + }); + + return () => { + unsubscribe(); + }; + } + }, [result.data, params.postID, subscribeToMore]); - return ( - - subscribeToMore({ - document: COMMENTS_SUBSCRIPTION, - variables: { postID: params.postID }, - updateQuery: (prev, { subscriptionData }) => { - if (!subscriptionData.data) return prev; - const newFeedItem = subscriptionData.data.commentAdded; - - return Object.assign({}, prev, { - post: { - comments: [newFeedItem, ...prev.post.comments] - } - }); - } - }) - } - /> - ); + return ; } ``` ```jsx {10-25} function CommentsPageWithData({ params }) { - const { subscribeToMore, ...result } = useQuery( - COMMENTS_QUERY, - { variables: { postID: params.postID } } - ); + const { subscribeToMore, ...result } = useQuery(COMMENTS_QUERY, { + variables: { postID: params.postID }, + }); + + useEffect(() => { + if (result.data) { + const unsubscribe = subscribeToMore({ + document: COMMENTS_SUBSCRIPTION, + variables: { postID: params.postID }, + updateQuery: (prev, { subscriptionData }) => { + if (!subscriptionData.data) return prev; + const newFeedItem = subscriptionData.data.commentAdded; + + return Object.assign({}, prev, { + post: { + comments: [newFeedItem, ...prev.post.comments], + }, + }); + }, + }); + + return () => { + unsubscribe(); + }; + } + }, [result.data, params.postID]); - return ( - - subscribeToMore({ - document: COMMENTS_SUBSCRIPTION, - variables: { postID: params.postID }, - updateQuery: (prev, { subscriptionData }) => { - if (!subscriptionData.data) return prev; - const newFeedItem = subscriptionData.data.commentAdded; - - return Object.assign({}, prev, { - post: { - comments: [newFeedItem, ...prev.post.comments] - } - }); - } - }) - } - /> - ); + return ; } ``` @@ -534,28 +540,6 @@ In the example above, we pass three options to `subscribeToMore`: * `variables` indicates the variables to include when executing the subscription. * `updateQuery` is a function that tells Apollo Client how to combine the query's currently cached result (`prev`) with the `subscriptionData` that's pushed by our GraphQL server. The return value of this function **completely replaces** the current cached result for the query. -Finally, in our definition of `CommentsPage`, we tell the component to `subscribeToNewComments` when it mounts: - - - -```tsx -export function CommentsPage({ subscribeToNewComments }: CommentsPageProps) { - useEffect(() => subscribeToNewComments(), []); - - return <>... -} -``` - -```jsx -export function CommentsPage({ subscribeToNewComments }) { - useEffect(() => subscribeToNewComments(), []); - - return <>... -} -``` - - - ## `useSubscription` API reference > **Note:** If you're using React Apollo's `Subscription` render prop component, the option/result details listed below are still valid (options are component props and results are passed into the render prop function). The only difference is that a `subscription` prop (which holds a GraphQL subscription document parsed into an AST by `gql`) is also required. diff --git a/docs/source/development-testing/schema-driven-testing.mdx b/docs/source/development-testing/schema-driven-testing.mdx index aa07ea39d65..f398814acfb 100644 --- a/docs/source/development-testing/schema-driven-testing.mdx +++ b/docs/source/development-testing/schema-driven-testing.mdx @@ -4,6 +4,14 @@ description: Using createTestSchema and associated APIs minVersion: 3.10.0 --- + + The testing utilities described in this document are deprecated and will be + removed in [Apollo Client + v4](https://community.apollographql.com/t/apollo-client-v4-alpha-releases-are-here/8719) + in June 2025. Please use [GraphQL Testing + Library](https://github.com/apollographql/graphql-testing-library) instead. + + 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 diff --git a/docs/source/development-testing/static-typing.md b/docs/source/development-testing/static-typing.mdx similarity index 100% rename from docs/source/development-testing/static-typing.md rename to docs/source/development-testing/static-typing.mdx diff --git a/docs/source/development-testing/testing.mdx b/docs/source/development-testing/testing.mdx index 3453f2559f4..c53ea71ca14 100644 --- a/docs/source/development-testing/testing.mdx +++ b/docs/source/development-testing/testing.mdx @@ -62,7 +62,7 @@ const mocks = []; // We'll fill this in next it("renders without error", async () => { render( - + ); @@ -87,7 +87,7 @@ const mocks = [ }, result: { data: { - dog: { id: "1", name: "Buck", breed: "bulldog" } + dog: { __typename: "Dog", id: "1", name: "Buck", breed: "bulldog" } } } } @@ -106,7 +106,7 @@ result: (variables) => { // `variables` is optional return { data: { - dog: { id: '1', name: 'Buck', breed: 'bulldog' }, + dog: { __typename: 'Dog', id: '1', name: 'Buck', breed: 'bulldog' }, }, } }, @@ -132,7 +132,7 @@ const mocks = [ }, result: { data: { - dog: { id: "1", name: "Buck", breed: "bulldog" } + dog: { __typename: "Dog", id: "1", name: "Buck", breed: "bulldog" } } } } @@ -140,7 +140,7 @@ const mocks = [ it("renders without error", async () => { render( - + ); @@ -169,7 +169,7 @@ const mocks = [ }, result: { data: { - dog: { id: "1", name: "Buck", breed: "bulldog" } + dog: { __typename: "Dog", id: "1", name: "Buck", breed: "bulldog" } } }, maxUsageCount: 2, // The mock can be used twice before it's removed, default is 1 @@ -196,7 +196,7 @@ const dogMock: MockedResponse = { }, variableMatcher: (variables) => true, result: { - data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } }, + data: { dog: { __typename: 'Dog', id: 1, name: 'Buck', breed: 'poodle' } }, }, }; ``` @@ -212,7 +212,7 @@ const dogMock: MockedResponse = { }, variableMatcher: jest.fn().mockReturnValue(true), result: { - data: { dog: { id: 1, name: 'Buck', breed: 'poodle' } }, + data: { dog: { __typename: 'Dog', id: 1, name: 'Buck', breed: 'poodle' } }, }, }; @@ -221,14 +221,6 @@ expect(variableMatcher).toHaveBeenCalledWith(expect.objectContaining({ })); ``` -### Setting `addTypename` - -In the example above, we set the `addTypename` prop of `MockedProvider` to `false`. This prevents Apollo Client from automatically adding the special `__typename` field to every object it queries for (it does this by default to support data normalization in the cache). - -We _don't_ want to automatically add `__typename` to `GET_DOG_QUERY` in our test, because then it won't match the shape of the query that our mock is expecting. - -Unless you explicitly configure your mocks to expect a `__typename` field, always set `addTypename` to `false` in your tests. - ## Testing the "loading" and "success" states To test how your component is rendered after its query completes, Testing Library provides several `findBy` methods. From the [Testing Library docs](https://testing-library.com/docs/dom-testing-library/api-async/#findby-queries): @@ -248,11 +240,11 @@ it("should render dog", async () => { variables: { name: "Buck" } }, result: { - data: { dog: { id: 1, name: "Buck", breed: "poodle" } } + data: { dog: { __typename: "Dog", id: 1, name: "Buck", breed: "poodle" } } } }; render( - + ); @@ -282,7 +274,7 @@ it("should show error UI", async () => { error: new Error("An error occurred") }; render( - + ); @@ -368,7 +360,7 @@ The following test _does_ execute the mutation by clicking the button: ```jsx title="delete-dog.test.js" it("should render loading and success states on delete", async () => { - const deleteDog = { name: "Buck", breed: "Poodle", id: 1 }; + const deleteDog = { __typename: "Dog", name: "Buck", breed: "Poodle", id: 1 }; const mocks = [ { request: { @@ -380,7 +372,7 @@ it("should render loading and success states on delete", async () => { ]; render( - + ); diff --git a/docs/source/integrations/integrations.md b/docs/source/integrations/integrations.mdx similarity index 100% rename from docs/source/integrations/integrations.md rename to docs/source/integrations/integrations.mdx diff --git a/docs/source/integrations/react-native.md b/docs/source/integrations/react-native.mdx similarity index 100% rename from docs/source/integrations/react-native.md rename to docs/source/integrations/react-native.mdx diff --git a/docs/source/integrations/webpack.md b/docs/source/integrations/webpack.mdx similarity index 97% rename from docs/source/integrations/webpack.md rename to docs/source/integrations/webpack.mdx index 99a578b99b0..12083ae97cd 100644 --- a/docs/source/integrations/webpack.md +++ b/docs/source/integrations/webpack.mdx @@ -47,7 +47,7 @@ export default graphql(currentUserQuery)(Profile) ## Jest -[Jest](https://facebook.github.io/jest/) can't use the Webpack loaders. To make the same transformation work in Jest, use [jest-transform-graphql](https://github.com/remind101/jest-transform-graphql). +[Jest](https://facebook.github.io/jest/) can't use the Webpack loaders. To make the same transformation work in Jest, use [jest-graphql-transformer](https://github.com/hamidyfine/jest-graphql-transformer). ## FuseBox diff --git a/docs/source/local-state/local-resolvers.mdx b/docs/source/local-state/local-resolvers.mdx index 5a086b351f7..ff5b643ebac 100644 --- a/docs/source/local-state/local-resolvers.mdx +++ b/docs/source/local-state/local-resolvers.mdx @@ -3,10 +3,6 @@ title: Local resolvers description: Manage local data with GraphQL like resolvers --- -> 📄 **NOTE:** We recommend using field policies instead of local resolvers as described in [Local-only fields](./managing-state-with-field-policies/). -> -> Local resolver support will be moved out of the core of Apollo Client in a future major release. The same or similar functionality will be available via `ApolloLink`, as described in [this issue](https://github.com/apollographql/apollo-client/issues/10060). - We've learned how to manage remote data from our GraphQL server with Apollo Client, but what should we do with our local data? We want to be able to access boolean flags and device API results from multiple components in our app, but don't want to maintain a separate Redux or MobX store. Ideally, we would like the Apollo cache to be the single source of truth for all data in our client application. Apollo Client (>= 2.5) has built-in local state handling capabilities that allow you to store your local data inside the Apollo cache alongside your remote data. To access your local data, just query it with GraphQL. You can even request local and server data within the same query! diff --git a/docs/source/migrating/hooks-migration.md b/docs/source/migrating/hooks-migration.mdx similarity index 100% rename from docs/source/migrating/hooks-migration.md rename to docs/source/migrating/hooks-migration.mdx diff --git a/docs/source/networking/advanced-http-networking.md b/docs/source/networking/advanced-http-networking.mdx similarity index 100% rename from docs/source/networking/advanced-http-networking.md rename to docs/source/networking/advanced-http-networking.mdx diff --git a/docs/source/networking/basic-http-networking.md b/docs/source/networking/basic-http-networking.mdx similarity index 100% rename from docs/source/networking/basic-http-networking.md rename to docs/source/networking/basic-http-networking.mdx diff --git a/docs/source/performance/babel.md b/docs/source/performance/babel.mdx similarity index 100% rename from docs/source/performance/babel.md rename to docs/source/performance/babel.mdx diff --git a/docs/source/template.md b/docs/source/template.md deleted file mode 100644 index 1137aee4f34..00000000000 --- a/docs/source/template.md +++ /dev/null @@ -1,17 +0,0 @@ ---- -title: Title -subtitle: Subtitle -description: Description ---- - -## Topic - -### Sub Topic - -```js -// code block -``` - -## Best Practices - -## API Documentation diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 97d5f4bb3d1..00000000000 --- a/netlify.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build] - publish = "docs/public" - command = "npm run typedoc; npm run docmodel > docs/public/log.txt || true" diff --git a/package-lock.json b/package-lock.json index 4949c78795e..f5fbd872075 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@apollo/client", - "version": "3.12.8", + "version": "3.13.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@apollo/client", - "version": "3.12.8", + "version": "3.13.8", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -25,7 +25,7 @@ "zen-observable-ts": "^1.2.5" }, "devDependencies": { - "@actions/github-script": "github:actions/github-script#v7", + "@actions/github-script": "github:actions/github-script#v7.0.1", "@arethetypeswrong/cli": "0.15.3", "@ark/attest": "0.28.0", "@babel/parser": "7.25.0", @@ -83,7 +83,7 @@ "globals": "15.14.0", "graphql": "16.9.0", "graphql-17-alpha2": "npm:graphql@17.0.0-alpha.2", - "graphql-ws": "5.16.0", + "graphql-ws": "6.0.3", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-junit": "16.0.0", @@ -124,7 +124,7 @@ }, "peerDependencies": { "graphql": "^15.0.0 || ^16.0.0", - "graphql-ws": "^5.5.5", + "graphql-ws": "^5.5.5 || ^6.0.3", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" @@ -7997,18 +7997,30 @@ } }, "node_modules/graphql-ws": { - "version": "5.16.0", - "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.16.0.tgz", - "integrity": "sha512-Ju2RCU2dQMgSKtArPbEtsK5gNLnsQyTNIo/T7cZNp96niC1x0KdJNZV0TIoilceBPQwfb5itrGl8pkFeOUMl4A==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-6.0.3.tgz", + "integrity": "sha512-mvLRHihMg0llF74vo16063HufZHMGaiMxAjzyj0ARYueIikGzj1khlbPNl7vUc2h9rxbq9pGpQYbqypgq1fAXA==", "dev": true, - "workspaces": [ - "website" - ], + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=20" }, "peerDependencies": { - "graphql": ">=0.11 <=16" + "@fastify/websocket": "^10 || ^11", + "graphql": "^15.10.1 || ^16", + "uWebSockets.js": "^20", + "ws": "^8" + }, + "peerDependenciesMeta": { + "@fastify/websocket": { + "optional": true + }, + "uWebSockets.js": { + "optional": true + }, + "ws": { + "optional": true + } } }, "node_modules/has": { diff --git a/package.json b/package.json index b5ad466cc0e..8294b098ebb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@apollo/client", - "version": "3.12.8", + "version": "3.13.8", "description": "A fully-featured caching GraphQL client.", "private": true, "keywords": [ @@ -73,7 +73,7 @@ }, "peerDependencies": { "graphql": "^15.0.0 || ^16.0.0", - "graphql-ws": "^5.5.5", + "graphql-ws": "^5.5.5 || ^6.0.3", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc", "subscriptions-transport-ws": "^0.9.0 || ^0.11.0" @@ -166,7 +166,7 @@ "globals": "15.14.0", "graphql": "16.9.0", "graphql-17-alpha2": "npm:graphql@17.0.0-alpha.2", - "graphql-ws": "5.16.0", + "graphql-ws": "6.0.3", "jest": "29.7.0", "jest-environment-jsdom": "29.7.0", "jest-junit": "16.0.0", diff --git a/src/__tests__/ApolloClient.ts b/src/__tests__/ApolloClient.ts index 72d551c4b9b..fce6521d2af 100644 --- a/src/__tests__/ApolloClient.ts +++ b/src/__tests__/ApolloClient.ts @@ -11,7 +11,7 @@ import { } from "../core"; import { Kind } from "graphql"; -import { Observable } from "../utilities"; +import { DeepPartial, Observable } from "../utilities"; import { ApolloLink, FetchResult } from "../link/core"; import { HttpLink } from "../link/http"; import { createFragmentRegistry, InMemoryCache } from "../cache"; @@ -3218,25 +3218,39 @@ describe("ApolloClient", () => { UnmaskedQuery | undefined >(); - observableQuery.updateQuery((previousData) => { - expectTypeOf(previousData).toMatchTypeOf(); - expectTypeOf(previousData).not.toMatchTypeOf(); + observableQuery.updateQuery( + (_previousData, { complete, previousData }) => { + expectTypeOf(_previousData).toEqualTypeOf(); + expectTypeOf(_previousData).not.toMatchTypeOf(); - return {} as UnmaskedQuery; - }); + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + } + ); observableQuery.subscribeToMore({ document: subscription, - updateQuery(queryData, { subscriptionData }) { - expectTypeOf(queryData).toMatchTypeOf(); + updateQuery(queryData, { subscriptionData, complete, previousData }) { + expectTypeOf(queryData).toEqualTypeOf(); expectTypeOf(queryData).not.toMatchTypeOf(); + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + expectTypeOf( subscriptionData.data ).toMatchTypeOf(); expectTypeOf(subscriptionData.data).not.toMatchTypeOf(); - - return {} as UnmaskedQuery; }, }); }); diff --git a/src/__tests__/__snapshots__/exports.ts.snap b/src/__tests__/__snapshots__/exports.ts.snap index e21e634c864..379695ffb14 100644 --- a/src/__tests__/__snapshots__/exports.ts.snap +++ b/src/__tests__/__snapshots__/exports.ts.snap @@ -67,6 +67,7 @@ Array [ "useReactiveVar", "useReadQuery", "useSubscription", + "useSuspenseFragment", "useSuspenseQuery", ] `; @@ -293,6 +294,7 @@ Array [ "useReactiveVar", "useReadQuery", "useSubscription", + "useSuspenseFragment", "useSuspenseQuery", ] `; @@ -338,6 +340,7 @@ Array [ "useReactiveVar", "useReadQuery", "useSubscription", + "useSuspenseFragment", "useSuspenseQuery", ] `; diff --git a/src/__tests__/client.ts b/src/__tests__/client.ts index 7e99eed1c6a..bef271b8ebb 100644 --- a/src/__tests__/client.ts +++ b/src/__tests__/client.ts @@ -3382,9 +3382,6 @@ describe("@connection", () => { // nextFetchPolicy function ends up getting called twice. void obs.refetch(); - await expect(stream).toEmitMatchedValue({ data: { count: "initial" } }); - expect(nextFetchPolicyCallCount).toBe(2); - await expect(stream).toEmitMatchedValue({ data: { count: 0 } }); expect(nextFetchPolicyCallCount).toBe(2); diff --git a/src/__tests__/dataMasking.ts b/src/__tests__/dataMasking.ts index 9c23822916b..8c386b67e6f 100644 --- a/src/__tests__/dataMasking.ts +++ b/src/__tests__/dataMasking.ts @@ -1891,8 +1891,16 @@ describe("client.watchQuery", () => { } const updateQuery: Parameters[0] = jest.fn( - (previousResult) => { - return { user: { ...previousResult.user, name: "User (updated)" } }; + (previousResult, { complete, previousData }) => { + expect(complete).toBe(true); + expect(previousData).toStrictEqual(previousResult); + // Type Guard + if (!complete) { + return; + } + return { + user: { ...previousData.user, name: "User (updated)" }, + }; } ); @@ -1900,7 +1908,13 @@ describe("client.watchQuery", () => { expect(updateQuery).toHaveBeenCalledWith( { user: { __typename: "User", id: 1, name: "User 1", age: 30 } }, - { variables: { id: 1 } } + { + variables: { id: 1 }, + complete: true, + previousData: { + user: { __typename: "User", id: 1, name: "User 1", age: 30 }, + }, + } ); { @@ -4831,7 +4845,16 @@ describe("observableQuery.subscribeToMore", () => { }, }, { + complete: true, variables: {}, + previousData: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, subscriptionData: { data: { addedComment: { @@ -4958,7 +4981,16 @@ describe("observableQuery.subscribeToMore", () => { }, }, { + complete: true, variables: {}, + previousData: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, subscriptionData: { data: { addedComment: { @@ -5006,8 +5038,8 @@ describe("observableQuery.subscribeToMore", () => { `; const subscription = gql` - subscription NewCommentSubscription { - addedComment { + subscription NewCommentSubscription($id: ID!) { + addedComment(id: $id) { id ...CommentFields } @@ -5060,7 +5092,11 @@ describe("observableQuery.subscribeToMore", () => { return { recentComment: subscriptionData.data.addedComment }; }); - observable.subscribeToMore({ document: subscription, updateQuery }); + observable.subscribeToMore({ + document: subscription, + updateQuery, + variables: { id: 1 }, + }); subscriptionLink.simulateResult({ result: { @@ -5087,7 +5123,16 @@ describe("observableQuery.subscribeToMore", () => { }, }, { + complete: true, variables: {}, + previousData: { + recentComment: { + __typename: "Comment", + id: 1, + comment: "Recent comment", + author: "Test User", + }, + }, subscriptionData: { data: { addedComment: { diff --git a/src/__tests__/fetchMore.ts b/src/__tests__/fetchMore.ts index 9d53d363789..56f3c50a377 100644 --- a/src/__tests__/fetchMore.ts +++ b/src/__tests__/fetchMore.ts @@ -22,7 +22,7 @@ import { } from "../cache"; import { MockedResponse, mockSingleLink } from "../testing"; -import { ObservableStream } from "../testing/internal"; +import { ObservableStream, setupPaginatedCase } from "../testing/internal"; describe("updateQuery on a simple query", () => { const query = gql` @@ -1789,3 +1789,140 @@ describe("fetchMore on an observable query with connection", () => { }); }); }); + +test("uses updateQuery to update the result of the query with no-cache queries", async () => { + const { query, link } = setupPaginatedCase(); + + const client = new ApolloClient({ cache: new InMemoryCache(), link }); + + const observable = client.watchQuery({ + query, + fetchPolicy: "no-cache", + notifyOnNetworkStatusChange: true, + variables: { limit: 2 }, + }); + + const stream = new ObservableStream(observable); + + await expect(stream).toEmitApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + let fetchMoreResult = await observable.fetchMore({ + variables: { offset: 2 }, + updateQuery: (prev, { fetchMoreResult }) => ({ + letters: prev.letters.concat(fetchMoreResult.letters), + }), + }); + + expect(fetchMoreResult).toEqualApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + ], + }, + loading: true, + networkStatus: NetworkStatus.fetchMore, + }); + + await expect(stream).toEmitApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + // Ensure we store the merged result as the last result + expect(observable.getCurrentResult(false)).toEqualApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); + + fetchMoreResult = await observable.fetchMore({ + variables: { offset: 4 }, + updateQuery: (_, { fetchMoreResult }) => fetchMoreResult, + }); + + expect(fetchMoreResult).toEqualApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "E", position: 5 }, + { __typename: "Letter", letter: "F", position: 6 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).toEmitApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "A", position: 1 }, + { __typename: "Letter", letter: "B", position: 2 }, + { __typename: "Letter", letter: "C", position: 3 }, + { __typename: "Letter", letter: "D", position: 4 }, + ], + }, + loading: true, + networkStatus: NetworkStatus.fetchMore, + }); + + await expect(stream).toEmitApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "E", position: 5 }, + { __typename: "Letter", letter: "F", position: 6 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + expect(observable.getCurrentResult(false)).toEqualApolloQueryResult({ + data: { + letters: [ + { __typename: "Letter", letter: "E", position: 5 }, + { __typename: "Letter", letter: "F", position: 6 }, + ], + }, + loading: false, + networkStatus: NetworkStatus.ready, + }); + + await expect(stream).not.toEmitAnything(); +}); diff --git a/src/__tests__/subscribeToMore.ts b/src/__tests__/subscribeToMore.ts index 6216eaa85c9..9761ee3f5a6 100644 --- a/src/__tests__/subscribeToMore.ts +++ b/src/__tests__/subscribeToMore.ts @@ -240,10 +240,17 @@ describe("subscribeToMore", () => { name } `, - updateQuery: (prev, { subscriptionData }) => { - expect(prev.entry).not.toContainEqual(nextMutation); + updateQuery: (prev, { subscriptionData, complete, previousData }) => { + expect(complete).toBe(true); + expect(previousData).toStrictEqual(prev); + // Type Guard + if (!complete) { + return; + } + + expect(previousData.entry).not.toContainEqual(nextMutation); return { - entry: [...prev.entry, { value: subscriptionData.data.name }], + entry: [...previousData.entry, { value: subscriptionData.data.name }], }; }, }); diff --git a/src/config/jest/setup.ts b/src/config/jest/setup.ts index 141d0e4132d..4c1ed3e5581 100644 --- a/src/config/jest/setup.ts +++ b/src/config/jest/setup.ts @@ -36,3 +36,6 @@ if (!Symbol.asyncDispose) { // @ts-ignore expect.addEqualityTesters([areApolloErrorsEqual, areGraphQLErrorsEqual]); + +// not available in JSDOM 🙄 +global.structuredClone = (val) => JSON.parse(JSON.stringify(val)); diff --git a/src/core/ObservableQuery.ts b/src/core/ObservableQuery.ts index 2a70e6e9097..889d4501c9b 100644 --- a/src/core/ObservableQuery.ts +++ b/src/core/ObservableQuery.ts @@ -31,6 +31,8 @@ import type { SubscribeToMoreOptions, NextFetchPolicyContext, WatchQueryFetchPolicy, + UpdateQueryMapFn, + UpdateQueryOptions, } from "./watchQueryOptions.js"; import type { QueryInfo } from "./QueryInfo.js"; import type { MissingFieldError } from "../cache/index.js"; @@ -38,6 +40,7 @@ import type { MissingTree } from "../cache/core/types/common.js"; import { equalByQuery } from "./equalByQuery.js"; import type { TODO } from "../utilities/types/TODO.js"; import type { MaybeMasked, Unmasked } from "../masking/index.js"; +import { Slot } from "optimism"; const { assign, hasOwnProperty } = Object; @@ -54,10 +57,6 @@ export interface FetchMoreOptions< ) => TData; } -export interface UpdateQueryOptions { - variables?: TVariables; -} - interface Last { result: ApolloQueryResult; variables?: TVariables; @@ -68,6 +67,15 @@ export class ObservableQuery< TData = any, TVariables extends OperationVariables = OperationVariables, > extends Observable>> { + /** + * @internal + * A slot used by the `useQuery` hook to indicate that `client.watchQuery` + * should not register the query immediately, but instead wait for the query to + * be started registered with the `QueryManager` when `useSyncExternalStore` + * actively subscribes to it. + */ + private static inactiveOnCreation = new Slot(); + public readonly options: WatchQueryOptions; public readonly queryId: string; public readonly queryName?: string; @@ -121,7 +129,13 @@ export class ObservableQuery< queryInfo: QueryInfo; options: WatchQueryOptions; }) { + let startedInactive = ObservableQuery.inactiveOnCreation.getValue(); super((observer: Observer>>) => { + if (startedInactive) { + queryManager["queries"].set(this.queryId, queryInfo); + startedInactive = false; + } + // Zen Observable has its own error function, so in order to log correctly // we need to provide a custom error callback. try { @@ -305,6 +319,17 @@ export class ObservableQuery< result.partial = true; } + // We need to check for both both `error` and `errors` field because there + // are cases where sometimes `error` is set, but not `errors` and + // vice-versa. This will be updated in the next major version when + // `errors` is deprecated in favor of `error`. + if ( + result.networkStatus === NetworkStatus.ready && + (result.error || result.errors) + ) { + result.networkStatus = NetworkStatus.error; + } + if ( __DEV__ && !diff.complete && @@ -406,9 +431,7 @@ export class ObservableQuery< // (no-cache, network-only, or cache-and-network), override it with // network-only to force the refetch for this fetchQuery call. const { fetchPolicy } = this.options; - if (fetchPolicy === "cache-and-network") { - reobserveOptions.fetchPolicy = fetchPolicy; - } else if (fetchPolicy === "no-cache") { + if (fetchPolicy === "no-cache") { reobserveOptions.fetchPolicy = "no-cache"; } else { reobserveOptions.fetchPolicy = "network-only"; @@ -585,7 +608,12 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, }); this.reportResult( - { ...lastResult, data: data as TData }, + { + ...lastResult, + networkStatus: originalNetworkStatus!, + loading: isNetworkRequestInFlight(originalNetworkStatus), + data: data as TData, + }, this.variables ); } @@ -599,7 +627,7 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, // the cache, we still want fetchMore to deliver its final loading:false // result with the unchanged data. if (isCached && !updatedQuerySet.has(this.query)) { - reobserveCacheFirst(this); + this.reobserveCacheFirst(); } }); } @@ -619,9 +647,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, options: SubscribeToMoreOptions< TData, TSubscriptionVariables, - TSubscriptionData + TSubscriptionData, + TVariables > - ) { + ): () => void { const subscription = this.queryManager .startGraphQLSubscription({ query: options.document, @@ -632,12 +661,11 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, next: (subscriptionData: { data: Unmasked }) => { const { updateQuery } = options; if (updateQuery) { - this.updateQuery( - (previous, { variables }) => - updateQuery(previous, { - subscriptionData, - variables, - }) + this.updateQuery((previous, updateOptions) => + updateQuery(previous, { + subscriptionData, + ...updateOptions, + }) ); } }, @@ -722,23 +750,23 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, * * See [using updateQuery and updateFragment](https://www.apollographql.com/docs/react/caching/cache-interaction/#using-updatequery-and-updatefragment) for additional information. */ - public updateQuery( - mapFn: ( - previousQueryResult: Unmasked, - options: Pick, "variables"> - ) => Unmasked - ): void { + public updateQuery(mapFn: UpdateQueryMapFn): void { const { queryManager } = this; - const { result } = queryManager.cache.diff({ + const { result, complete } = queryManager.cache.diff({ query: this.options.query, variables: this.variables, returnPartialData: true, optimistic: false, }); - const newResult = mapFn(result! as Unmasked, { - variables: (this as any).variables, - }); + const newResult = mapFn( + result! as Unmasked, + { + variables: this.variables, + complete: !!complete, + previousData: result, + } as UpdateQueryOptions + ); if (newResult) { queryManager.cache.writeQuery({ @@ -816,9 +844,10 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, ) { // TODO Make sure we update the networkStatus (and infer fetchVariables) // before actually committing to the fetch. - this.queryManager.setObservableQuery(this); + const queryInfo = this.queryManager.getOrCreateQuery(this.queryId); + queryInfo.setObservableQuery(this); return this.queryManager["fetchConcastWithInfo"]( - this.queryId, + queryInfo, options, newNetworkStatus, query @@ -1146,51 +1175,110 @@ Did you mean to call refetch(variables) instead of refetch({ variables })?`, } : result; } -} -// Necessary because the ObservableQuery constructor has a different -// signature than the Observable constructor. -fixObservableSubclass(ObservableQuery); + private dirty: boolean = false; -// Reobserve with fetchPolicy effectively set to "cache-first", triggering -// delivery of any new data from the cache, possibly falling back to the network -// if any cache data are missing. This allows _complete_ cache results to be -// delivered without also kicking off unnecessary network requests when -// this.options.fetchPolicy is "cache-and-network" or "network-only". When -// this.options.fetchPolicy is any other policy ("cache-first", "cache-only", -// "standby", or "no-cache"), we call this.reobserve() as usual. -export function reobserveCacheFirst( - obsQuery: ObservableQuery -) { - const { fetchPolicy, nextFetchPolicy } = obsQuery.options; - - if (fetchPolicy === "cache-and-network" || fetchPolicy === "network-only") { - return obsQuery.reobserve({ - fetchPolicy: "cache-first", - // Use a temporary nextFetchPolicy function that replaces itself with the - // previous nextFetchPolicy value and returns the original fetchPolicy. - nextFetchPolicy( - this: WatchQueryOptions, - currentFetchPolicy: WatchQueryFetchPolicy, - context: NextFetchPolicyContext + private notifyTimeout?: ReturnType; + + /** @internal */ + protected resetNotifications() { + this.cancelNotifyTimeout(); + this.dirty = false; + } + + private cancelNotifyTimeout() { + if (this.notifyTimeout) { + clearTimeout(this.notifyTimeout); + this.notifyTimeout = void 0; + } + } + + /** @internal */ + protected scheduleNotify() { + if (this.dirty) return; + this.dirty = true; + if (!this.notifyTimeout) { + this.notifyTimeout = setTimeout(() => this.notify(), 0); + } + } + + /** @internal */ + protected notify() { + this.cancelNotifyTimeout(); + + if (this.dirty) { + if ( + this.options.fetchPolicy == "cache-only" || + this.options.fetchPolicy == "cache-and-network" || + !isNetworkRequestInFlight(this.queryInfo.networkStatus) ) { - // Replace this nextFetchPolicy function in the options object with the - // original this.options.nextFetchPolicy value. - this.nextFetchPolicy = nextFetchPolicy; - // If the original nextFetchPolicy value was a function, give it a - // chance to decide what happens here. - if (typeof this.nextFetchPolicy === "function") { - return this.nextFetchPolicy(currentFetchPolicy, context); + const diff = this.queryInfo.getDiff(); + if (diff.fromOptimisticTransaction) { + // If this diff came from an optimistic transaction, deliver the + // current cache data to the ObservableQuery, but don't perform a + // reobservation, since oq.reobserveCacheFirst might make a network + // request, and we never want to trigger network requests in the + // middle of optimistic updates. + this.observe(); + } else { + // Otherwise, make the ObservableQuery "reobserve" the latest data + // using a temporary fetch policy of "cache-first", so complete cache + // results have a chance to be delivered without triggering additional + // network requests, even when options.fetchPolicy is "network-only" + // or "cache-and-network". All other fetch policies are preserved by + // this method, and are handled by calling oq.reobserve(). If this + // reobservation is spurious, isDifferentFromLastResult still has a + // chance to catch it before delivery to ObservableQuery subscribers. + this.reobserveCacheFirst(); } - // Otherwise go back to the original this.options.fetchPolicy. - return fetchPolicy!; - }, - }); + } + } + + this.dirty = false; } - return obsQuery.reobserve(); + // Reobserve with fetchPolicy effectively set to "cache-first", triggering + // delivery of any new data from the cache, possibly falling back to the network + // if any cache data are missing. This allows _complete_ cache results to be + // delivered without also kicking off unnecessary network requests when + // this.options.fetchPolicy is "cache-and-network" or "network-only". When + // this.options.fetchPolicy is any other policy ("cache-first", "cache-only", + // "standby", or "no-cache"), we call this.reobserve() as usual. + private reobserveCacheFirst() { + const { fetchPolicy, nextFetchPolicy } = this.options; + + if (fetchPolicy === "cache-and-network" || fetchPolicy === "network-only") { + return this.reobserve({ + fetchPolicy: "cache-first", + // Use a temporary nextFetchPolicy function that replaces itself with the + // previous nextFetchPolicy value and returns the original fetchPolicy. + nextFetchPolicy( + this: WatchQueryOptions, + currentFetchPolicy: WatchQueryFetchPolicy, + context: NextFetchPolicyContext + ) { + // Replace this nextFetchPolicy function in the options object with the + // original this.options.nextFetchPolicy value. + this.nextFetchPolicy = nextFetchPolicy; + // If the original nextFetchPolicy value was a function, give it a + // chance to decide what happens here. + if (typeof this.nextFetchPolicy === "function") { + return this.nextFetchPolicy(currentFetchPolicy, context); + } + // Otherwise go back to the original this.options.fetchPolicy. + return fetchPolicy!; + }, + }); + } + + return this.reobserve(); + } } +// Necessary because the ObservableQuery constructor has a different +// signature than the Observable constructor. +fixObservableSubclass(ObservableQuery); + function defaultSubscriptionObserverErrorCallback(error: ApolloError) { invariant.error("Unhandled error", error.message, error.stack); } diff --git a/src/core/QueryInfo.ts b/src/core/QueryInfo.ts index 2c065972b78..2a228eb8d13 100644 --- a/src/core/QueryInfo.ts +++ b/src/core/QueryInfo.ts @@ -6,15 +6,13 @@ import { DeepMerger } from "../utilities/index.js"; import { mergeIncrementalData } from "../utilities/index.js"; import type { WatchQueryOptions, ErrorPolicy } from "./watchQueryOptions.js"; import type { ObservableQuery } from "./ObservableQuery.js"; -import { reobserveCacheFirst } from "./ObservableQuery.js"; -import type { QueryListener } from "./types.js"; import type { FetchResult } from "../link/core/index.js"; import { isNonEmptyArray, graphQLResultHasError, canUseWeakMap, } from "../utilities/index.js"; -import { NetworkStatus, isNetworkRequestInFlight } from "./networkStatus.js"; +import { NetworkStatus } from "./networkStatus.js"; import type { ApolloError } from "../errors/index.js"; import type { QueryManager } from "./QueryManager.js"; import type { Unmasked } from "../masking/index.js"; @@ -57,13 +55,6 @@ function wrapDestructiveCacheMethod( } } -function cancelNotifyTimeout(info: QueryInfo) { - if (info["notifyTimeout"]) { - clearTimeout(info["notifyTimeout"]); - info["notifyTimeout"] = void 0; - } -} - // A QueryInfo object represents a single query managed by the // QueryManager, which tracks all QueryInfo objects by queryId in its // this.queries Map. QueryInfo objects store the latest results and errors @@ -77,7 +68,6 @@ function cancelNotifyTimeout(info: QueryInfo) { // many public fields. The effort to lock down and simplify the QueryInfo // interface is ongoing, and further improvements are welcome. export class QueryInfo { - listeners = new Set(); document: DocumentNode | null = null; lastRequestId = 1; variables?: Record; @@ -86,6 +76,7 @@ export class QueryInfo { graphQLErrors?: ReadonlyArray; stopped = false; + private cancelWatch?: () => void; private cache: ApolloCache; constructor( @@ -128,6 +119,8 @@ export class QueryInfo { if (!equal(query.variables, this.variables)) { this.lastDiff = void 0; + // Ensure we don't continue to receive cache updates for old variables + this.cancel(); } Object.assign(this, { @@ -149,15 +142,6 @@ export class QueryInfo { return this; } - private dirty: boolean = false; - - private notifyTimeout?: ReturnType; - - reset() { - cancelNotifyTimeout(this); - this.dirty = false; - } - resetDiff() { this.lastDiff = void 0; } @@ -227,79 +211,18 @@ export class QueryInfo { this.updateLastDiff(diff); - if (!this.dirty && !equal(oldDiff && oldDiff.result, diff && diff.result)) { - this.dirty = true; - if (!this.notifyTimeout) { - this.notifyTimeout = setTimeout(() => this.notify(), 0); - } + if (!equal(oldDiff && oldDiff.result, diff && diff.result)) { + this.observableQuery?.["scheduleNotify"](); } } public readonly observableQuery: ObservableQuery | null = null; - private oqListener?: QueryListener; - setObservableQuery(oq: ObservableQuery | null) { if (oq === this.observableQuery) return; - - if (this.oqListener) { - this.listeners.delete(this.oqListener); - } - (this as any).observableQuery = oq; - if (oq) { oq["queryInfo"] = this; - this.listeners.add( - (this.oqListener = () => { - const diff = this.getDiff(); - if (diff.fromOptimisticTransaction) { - // If this diff came from an optimistic transaction, deliver the - // current cache data to the ObservableQuery, but don't perform a - // reobservation, since oq.reobserveCacheFirst might make a network - // request, and we never want to trigger network requests in the - // middle of optimistic updates. - oq["observe"](); - } else { - // Otherwise, make the ObservableQuery "reobserve" the latest data - // using a temporary fetch policy of "cache-first", so complete cache - // results have a chance to be delivered without triggering additional - // network requests, even when options.fetchPolicy is "network-only" - // or "cache-and-network". All other fetch policies are preserved by - // this method, and are handled by calling oq.reobserve(). If this - // reobservation is spurious, isDifferentFromLastResult still has a - // chance to catch it before delivery to ObservableQuery subscribers. - reobserveCacheFirst(oq); - } - }) - ); - } else { - delete this.oqListener; - } - } - - notify() { - cancelNotifyTimeout(this); - - if (this.shouldNotify()) { - this.listeners.forEach((listener) => listener(this)); } - - this.dirty = false; - } - - private shouldNotify() { - if (!this.dirty || !this.listeners.size) { - return false; - } - - if (isNetworkRequestInFlight(this.networkStatus) && this.observableQuery) { - const { fetchPolicy } = this.observableQuery.options; - if (fetchPolicy !== "cache-only" && fetchPolicy !== "cache-and-network") { - return false; - } - } - - return true; } public stop() { @@ -307,21 +230,18 @@ export class QueryInfo { this.stopped = true; // Cancel the pending notify timeout - this.reset(); - + this.observableQuery?.["resetNotifications"](); this.cancel(); - // Revert back to the no-op version of cancel inherited from - // QueryInfo.prototype. - this.cancel = QueryInfo.prototype.cancel; const oq = this.observableQuery; if (oq) oq.stopPolling(); } } - // This method is a no-op by default, until/unless overridden by the - // updateWatch method. - private cancel() {} + private cancel() { + this.cancelWatch?.(); + this.cancelWatch = void 0; + } private lastWatch?: Cache.WatchOptions; @@ -342,7 +262,7 @@ export class QueryInfo { if (!this.lastWatch || !equal(watchOptions, this.lastWatch)) { this.cancel(); - this.cancel = this.cache.watch((this.lastWatch = watchOptions)); + this.cancelWatch = this.cache.watch((this.lastWatch = watchOptions)); } } @@ -387,7 +307,7 @@ export class QueryInfo { // Cancel the pending notify timeout (if it exists) to prevent extraneous network // requests. To allow future notify timeouts, diff and dirty are reset as well. - this.reset(); + this.observableQuery?.["resetNotifications"](); if ("incremental" in result && isNonEmptyArray(result.incremental)) { const mergedData = mergeIncrementalData(this.getDiff().result, result); @@ -513,7 +433,7 @@ export class QueryInfo { this.networkStatus = NetworkStatus.error; this.lastWrite = void 0; - this.reset(); + this.observableQuery?.["resetNotifications"](); if (error.graphQLErrors) { this.graphQLErrors = error.graphQLErrors; diff --git a/src/core/QueryManager.ts b/src/core/QueryManager.ts index 066dc137de9..fbd58b36d5a 100644 --- a/src/core/QueryManager.ts +++ b/src/core/QueryManager.ts @@ -658,8 +658,11 @@ export class QueryManager { options: WatchQueryOptions, networkStatus?: NetworkStatus ): Promise> { - return this.fetchConcastWithInfo(queryId, options, networkStatus).concast - .promise as TODO; + return this.fetchConcastWithInfo( + this.getOrCreateQuery(queryId), + options, + networkStatus + ).concast.promise as TODO; } public getQueryStore() { @@ -780,7 +783,9 @@ export class QueryManager { }); observable["lastQuery"] = query; - this.queries.set(observable.queryId, queryInfo); + if (!ObservableQuery["inactiveOnCreation"].getValue()) { + this.queries.set(observable.queryId, queryInfo); + } // We give queryInfo the transformed query to ensure the first cache diff // uses the transformed query instead of the raw query @@ -955,7 +960,7 @@ export class QueryManager { // pre-allocate a new query ID here, using a special prefix to enable // cleaning up these temporary queries later, after fetching. const queryId = makeUniqueId("legacyOneTimeQuery"); - const queryInfo = this.getQuery(queryId).init({ + const queryInfo = this.getOrCreateQuery(queryId).init({ document: options.query, variables: options.variables, }); @@ -1010,7 +1015,9 @@ export class QueryManager { ) { observableQueryPromises.push(observableQuery.refetch()); } - this.getQuery(queryId).setDiff(null); + (this.queries.get(queryId) || observableQuery["queryInfo"]).setDiff( + null + ); } ); @@ -1019,10 +1026,6 @@ export class QueryManager { return Promise.all(observableQueryPromises); } - public setObservableQuery(observableQuery: ObservableQuery) { - this.getQuery(observableQuery.queryId).setObservableQuery(observableQuery); - } - public startGraphQLSubscription( options: SubscriptionOptions ): Observable> { @@ -1118,14 +1121,14 @@ export class QueryManager { // The same queryId could have two rejection fns for two promises this.fetchCancelFns.delete(queryId); if (this.queries.has(queryId)) { - this.getQuery(queryId).stop(); + this.queries.get(queryId)?.stop(); this.queries.delete(queryId); } } public broadcastQueries() { if (this.onBroadcast) this.onBroadcast(); - this.queries.forEach((info) => info.notify()); + this.queries.forEach((info) => info.observableQuery?.["notify"]()); } public getLocalState(): LocalState { @@ -1182,8 +1185,12 @@ export class QueryManager { ]); observable = entry.observable = concast; - concast.beforeNext(() => { - inFlightLinkObservables.remove(printedServerQuery, varJson); + concast.beforeNext(function cb(method, arg: FetchResult) { + if (method === "next" && "hasNext" in arg && arg.hasNext) { + concast.beforeNext(cb); + } else { + inFlightLinkObservables.remove(printedServerQuery, varJson); + } }); } } else { @@ -1299,7 +1306,7 @@ export class QueryManager { } private fetchConcastWithInfo( - queryId: string, + queryInfo: QueryInfo, options: WatchQueryOptions, // The initial networkStatus for this fetch, most often // NetworkStatus.loading, but also possibly fetchMore, poll, refetch, @@ -1308,7 +1315,6 @@ export class QueryManager { query = options.query ): ConcastAndInfo { const variables = this.getVariables(query, options.variables) as TVars; - const queryInfo = this.getQuery(queryId); const defaults = this.defaultOptions.watchQuery; let { @@ -1361,8 +1367,8 @@ export class QueryManager { // This cancel function needs to be set before the concast is created, // in case concast creation synchronously cancels the request. - const cleanupCancelFn = () => this.fetchCancelFns.delete(queryId); - this.fetchCancelFns.set(queryId, (reason) => { + const cleanupCancelFn = () => this.fetchCancelFns.delete(queryInfo.queryId); + this.fetchCancelFns.set(queryInfo.queryId, (reason) => { cleanupCancelFn(); // This delay ensures the concast variable has been initialized. setTimeout(() => concast.cancel(reason)); @@ -1431,7 +1437,7 @@ export class QueryManager { this.getObservableQueries(include).forEach((oq, queryId) => { includedQueriesById.set(queryId, { oq, - lastDiff: this.getQuery(queryId).getDiff(), + lastDiff: (this.queries.get(queryId) || oq["queryInfo"]).getDiff(), }); }); } @@ -1539,9 +1545,7 @@ export class QueryManager { // queries, even the QueryOptions ones. if (onQueryUpdated) { if (!diff) { - const info = oq["queryInfo"]; - info.reset(); // Force info.getDiff() to read from cache. - diff = info.getDiff(); + diff = this.cache.diff(oq["queryInfo"]["getDiffOptions"]()); } result = onQueryUpdated(oq, diff, lastDiff); } @@ -1784,7 +1788,7 @@ export class QueryManager { } } - private getQuery(queryId: string): QueryInfo { + public getOrCreateQuery(queryId: string): QueryInfo { if (queryId && !this.queries.has(queryId)) { this.queries.set(queryId, new QueryInfo(this, queryId)); } diff --git a/src/core/__tests__/ApolloClient/general.test.ts b/src/core/__tests__/ApolloClient/general.test.ts index e18dab64fd7..26a749e6827 100644 --- a/src/core/__tests__/ApolloClient/general.test.ts +++ b/src/core/__tests__/ApolloClient/general.test.ts @@ -9,7 +9,11 @@ import { Observable, Observer, } from "../../../utilities/observables/Observable"; -import { ApolloLink, FetchResult } from "../../../link/core"; +import { + ApolloLink, + FetchResult, + type RequestHandler, +} from "../../../link/core"; import { InMemoryCache } from "../../../cache"; // mocks @@ -20,18 +24,24 @@ import { // core import { NetworkStatus } from "../../networkStatus"; -import { WatchQueryOptions } from "../../watchQueryOptions"; -import { QueryManager } from "../../QueryManager"; +import { + WatchQueryFetchPolicy, + WatchQueryOptions, +} from "../../watchQueryOptions"; import { ApolloError } from "../../../errors"; // testing utils import { waitFor } from "@testing-library/react"; import { wait } from "../../../testing/core"; -import { ApolloClient } from "../../../core"; +import { ApolloClient, ApolloQueryResult } from "../../../core"; import { mockFetchQuery } from "../ObservableQuery"; import { Concast, print } from "../../../utilities"; -import { ObservableStream, spyOnConsole } from "../../../testing/internal"; +import { + mockDeferStream, + ObservableStream, + spyOnConsole, +} from "../../../testing/internal"; describe("ApolloClient", () => { const getObservableStream = ({ @@ -865,23 +875,7 @@ describe("ApolloClient", () => { expect(data).toBe(observable.getCurrentResult().data); }); - it("sets networkStatus to `refetch` when refetching", async () => { - const request: WatchQueryOptions = { - query: gql` - query fetchLuke($id: String) { - people_one(id: $id) { - name - } - } - `, - variables: { - id: "1", - }, - notifyOnNetworkStatusChange: true, - // This causes a loading:true result to be delivered from the cache - // before the final data2 result is delivered. - fetchPolicy: "cache-and-network", - }; + { const data1 = { people_one: { name: "Luke Skywalker", @@ -893,37 +887,187 @@ describe("ApolloClient", () => { name: "Luke Skywalker has a new name", }, }; + it.each< + [ + notifyOnNetworkStatusChange: boolean, + fetchPolicy: WatchQueryFetchPolicy, + expectedInitialResults: ApolloQueryResult[], + expectedRefetchedResults: ApolloQueryResult[], + ] + >([ + [ + false, + "cache-first", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + [ + false, + "network-only", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + [ + false, + "cache-and-network", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + // { + // data: data1, + // loading: true, + // networkStatus: NetworkStatus.refetch, + // partial: false, + // }, + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + [ + true, + "cache-first", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + { + data: data1, + loading: true, + networkStatus: NetworkStatus.refetch, + }, + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + [ + true, + "network-only", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + { + data: data1, + loading: true, + networkStatus: NetworkStatus.refetch, + }, + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + [ + true, + "cache-and-network", + [ + { + data: data1, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + [ + { + data: data1, + loading: true, + networkStatus: NetworkStatus.refetch, + }, + { + data: data2, + loading: false, + networkStatus: NetworkStatus.ready, + }, + ], + ], + ])( + "networkStatus changes (notifyOnNetworkStatusChange: %s, fetchPolicy %s)", + async ( + notifyOnNetworkStatusChange, + fetchPolicy, + expectedInitialResults, + expectedRefetchedResults + ) => { + const request: WatchQueryOptions = { + query: gql` + query fetchLuke($id: String) { + people_one(id: $id) { + name + } + } + `, + variables: { + id: "1", + }, + notifyOnNetworkStatusChange, + fetchPolicy, + }; - const client = new ApolloClient({ - cache: new InMemoryCache({ addTypename: false }), - link: new MockLink([ - { request, result: { data: data1 } }, - { request, result: { data: data2 } }, - ]), - }); - - const observable = client.watchQuery(request); - const stream = new ObservableStream(observable); - - await expect(stream).toEmitApolloQueryResult({ - data: data1, - loading: false, - networkStatus: NetworkStatus.ready, - }); - - void observable.refetch(); + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { request, result: { data: data1 } }, + { request, result: { data: data2 } }, + ]), + }); - await expect(stream).toEmitApolloQueryResult({ - data: data1, - loading: true, - networkStatus: NetworkStatus.refetch, - }); - await expect(stream).toEmitApolloQueryResult({ - data: data2, - loading: false, - networkStatus: NetworkStatus.ready, - }); - }); + const observable = client.watchQuery(request); + const stream = new ObservableStream(observable); + for (const expected of expectedInitialResults) { + await expect(stream).toEmitApolloQueryResult(expected); + } + void observable.refetch(); + for (const expected of expectedRefetchedResults) { + await expect(stream).toEmitApolloQueryResult(expected); + } + expect(stream).not.toEmitAnything(); + } + ); + } it("allows you to refetch queries with promises", async () => { const request = { @@ -2724,16 +2868,13 @@ describe("ApolloClient", () => { const mocks = mockFetchQuery(queryManager); const queryId = "1"; - const getQuery: QueryManager["getQuery"] = ( - queryManager as any - ).getQuery.bind(queryManager); const stream = new ObservableStream(observable); await expect(stream).toEmitNext(); { - const query = getQuery(queryId); + const query = queryManager.getOrCreateQuery(queryId); const fqbpCalls = mocks.fetchQueryByPolicy.mock.calls; expect(query.lastRequestId).toEqual(1); @@ -6522,6 +6663,129 @@ describe("ApolloClient", () => { ) ).toBeUndefined(); }); + + it("deduplicates queries as long as a query still has deferred chunks", async () => { + const query = gql` + query LazyLoadLuke { + people(id: 1) { + id + name + friends { + id + ... @defer { + name + } + } + } + } + `; + + const outgoingRequestSpy = jest.fn(((operation, forward) => + forward(operation)) satisfies RequestHandler); + const defer = mockDeferStream(); + const client = new ApolloClient({ + cache: new InMemoryCache({}), + link: new ApolloLink(outgoingRequestSpy).concat(defer.httpLink), + }); + + const query1 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + const query2 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const initialData = { + people: { + __typename: "Person", + id: 1, + name: "Luke", + friends: [ + { + __typename: "Person", + id: 5, + } as { __typename: "Person"; id: number; name?: string }, + { + __typename: "Person", + id: 8, + } as { __typename: "Person"; id: number; name?: string }, + ], + }, + }; + const initialResult = { + data: initialData, + loading: false, + networkStatus: 7, + }; + + defer.enqueueInitialChunk({ + data: initialData, + hasNext: true, + }); + + await expect(query1).toEmitFetchResult(initialResult); + await expect(query2).toEmitFetchResult(initialResult); + + const query3 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + await expect(query3).toEmitFetchResult(initialResult); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const firstChunk = { + incremental: [ + { + data: { + name: "Leia", + }, + path: ["people", "friends", 0], + }, + ], + hasNext: true, + }; + const resultAfterFirstChunk = structuredClone(initialResult); + resultAfterFirstChunk.data.people.friends[0].name = "Leia"; + + defer.enqueueSubsequentChunk(firstChunk); + + await expect(query1).toEmitFetchResult(resultAfterFirstChunk); + await expect(query2).toEmitFetchResult(resultAfterFirstChunk); + await expect(query3).toEmitFetchResult(resultAfterFirstChunk); + + const query4 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + expect(query4).toEmitFetchResult(resultAfterFirstChunk); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(1); + + const secondChunk = { + incremental: [ + { + data: { + name: "Han Solo", + }, + path: ["people", "friends", 1], + }, + ], + hasNext: false, + }; + const resultAfterSecondChunk = structuredClone(resultAfterFirstChunk); + resultAfterSecondChunk.data.people.friends[1].name = "Han Solo"; + + defer.enqueueSubsequentChunk(secondChunk); + + await expect(query1).toEmitFetchResult(resultAfterSecondChunk); + await expect(query2).toEmitFetchResult(resultAfterSecondChunk); + await expect(query3).toEmitFetchResult(resultAfterSecondChunk); + await expect(query4).toEmitFetchResult(resultAfterSecondChunk); + + const query5 = new ObservableStream( + client.watchQuery({ query, fetchPolicy: "network-only" }) + ); + expect(query5).not.toEmitAnything(); + expect(outgoingRequestSpy).toHaveBeenCalledTimes(2); + }); }); describe("missing cache field warnings", () => { diff --git a/src/core/__tests__/ObservableQuery.ts b/src/core/__tests__/ObservableQuery.ts index 133165db818..21da5da9f7a 100644 --- a/src/core/__tests__/ObservableQuery.ts +++ b/src/core/__tests__/ObservableQuery.ts @@ -12,6 +12,7 @@ import { ObservableQuery } from "../ObservableQuery"; import { QueryManager } from "../QueryManager"; import { + DeepPartial, DocumentTransform, Observable, removeDirectivesFromDocument, @@ -21,6 +22,7 @@ import { InMemoryCache } from "../../cache"; import { ApolloError } from "../../errors"; import { MockLink, MockSubscriptionLink, tick, wait } from "../../testing"; +import { expectTypeOf } from "expect-type"; import { SubscriptionObserver } from "zen-observable-ts"; import { waitFor } from "@testing-library/react"; @@ -1001,7 +1003,7 @@ describe("ObservableQuery", () => { data: undefined, errors: [error], loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, // TODO: This is not present on the emitted result so this should match partial: true, }); @@ -1682,6 +1684,7 @@ describe("ObservableQuery", () => { query, fetchPolicy: "cache-and-network", returnPartialData: true, + notifyOnNetworkStatusChange: true, }); const stream = new ObservableStream(observable); @@ -1722,7 +1725,7 @@ describe("ObservableQuery", () => { expect(result).toEqualApolloQueryResult({ data: { - counter: 5, + counter: 4, name: "Ben", }, loading: false, @@ -2276,6 +2279,7 @@ describe("ObservableQuery", () => { const result = await observable.result(); const currentResult = observable.getCurrentResult(); + // TODO: This should include an `error` property, not just `errors` expect(result).toEqualApolloQueryResult({ data: dataOne, errors: [error], @@ -2286,9 +2290,7 @@ describe("ObservableQuery", () => { data: dataOne, errors: [error], loading: false, - // TODO: The networkStatus returned here is different than the one - // returned from `observable.result()`. These should match - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, }); }); @@ -2494,6 +2496,11 @@ describe("ObservableQuery", () => { loading: false, networkStatus: NetworkStatus.ready, }); + expect(observable.getCurrentResult()).toEqualApolloQueryResult({ + data: dataTwo, + loading: false, + networkStatus: NetworkStatus.ready, + }); await expect(stream).not.toEmitAnything(); }); @@ -3350,6 +3357,68 @@ describe("ObservableQuery", () => { }); }); + describe("updateQuery", () => { + it("should be able to determine if the previous result is complete", async () => { + const client = new ApolloClient({ + cache: new InMemoryCache({ addTypename: false }), + link: new MockLink([ + { + request: { query, variables }, + result: { data: dataOne }, + }, + ]), + }); + + const observable = client.watchQuery({ + query, + variables, + }); + + let updateQuerySpy = jest.fn(); + observable.updateQuery((previous, { complete, previousData }) => { + updateQuerySpy(); + expect(previous).toEqual({}); + expect(complete).toBe(false); + expect(previousData).toStrictEqual(previous); + + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + }); + + observable.subscribe(jest.fn()); + + await waitFor(() => { + expect(observable.getCurrentResult(false)).toEqual({ + data: dataOne, + loading: false, + networkStatus: NetworkStatus.ready, + }); + }); + + observable.updateQuery((previous, { complete, previousData }) => { + updateQuerySpy(); + expect(previous).toEqual(dataOne); + expect(complete).toBe(true); + expect(previousData).toStrictEqual(previous); + + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + }); + + expect(updateQuerySpy).toHaveBeenCalledTimes(2); + }); + }); + it("QueryInfo does not notify for !== but deep-equal results", async () => { const client = new ApolloClient({ cache: new InMemoryCache({ addTypename: false }), @@ -3374,7 +3443,10 @@ describe("ObservableQuery", () => { const queryInfo = observable["queryInfo"]; const cache = queryInfo["cache"]; const setDiffSpy = jest.spyOn(queryInfo, "setDiff"); - const notifySpy = jest.spyOn(queryInfo, "notify"); + const notifySpy = jest.spyOn( + observable, + "notify" as any /* this is not a public method so we cast */ + ); const stream = new ObservableStream(observable); diff --git a/src/core/index.ts b/src/core/index.ts index 3b0143ae6c1..9c6ac111d18 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -2,10 +2,7 @@ export type { ApolloClientOptions, DefaultOptions } from "./ApolloClient.js"; export { ApolloClient, mergeOptions } from "./ApolloClient.js"; -export type { - FetchMoreOptions, - UpdateQueryOptions, -} from "./ObservableQuery.js"; +export type { FetchMoreOptions } from "./ObservableQuery.js"; export { ObservableQuery } from "./ObservableQuery.js"; export type { QueryOptions, @@ -19,6 +16,10 @@ export type { ErrorPolicy, FetchMoreQueryOptions, SubscribeToMoreOptions, + SubscribeToMoreFunction, + UpdateQueryMapFn, + UpdateQueryOptions, + SubscribeToMoreUpdateQueryFn, } from "./watchQueryOptions.js"; export { NetworkStatus, isNetworkRequestSettled } from "./networkStatus.js"; export type * from "./types.js"; diff --git a/src/core/types.ts b/src/core/types.ts index 434b9d1a098..65ba13776a1 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -3,7 +3,6 @@ import type { DocumentNode, GraphQLFormattedError } from "graphql"; import type { ApolloCache } from "../cache/index.js"; import type { FetchResult } from "../link/core/index.js"; import type { ApolloError } from "../errors/index.js"; -import type { QueryInfo } from "./QueryInfo.js"; import type { NetworkStatus } from "./networkStatus.js"; import type { Resolver } from "./LocalState.js"; import type { ObservableQuery } from "./ObservableQuery.js"; @@ -20,8 +19,6 @@ export type MethodKeys = { export interface DefaultContext extends Record {} -export type QueryListener = (queryInfo: QueryInfo) => void; - export type OnQueryUpdated = ( observableQuery: ObservableQuery, diff: Cache.DiffResult, diff --git a/src/core/watchQueryOptions.ts b/src/core/watchQueryOptions.ts index 1528b1d0330..f03448a73a0 100644 --- a/src/core/watchQueryOptions.ts +++ b/src/core/watchQueryOptions.ts @@ -14,7 +14,7 @@ import type { ApolloCache } from "../cache/index.js"; import type { ObservableQuery } from "./ObservableQuery.js"; import type { IgnoreModifier } from "../cache/core/types/common.js"; import type { Unmasked } from "../masking/index.js"; -import type { NoInfer } from "../utilities/index.js"; +import type { DeepPartial, NoInfer } from "../utilities/index.js"; /** * fetchPolicy determines where the client may return a result from. The options are: @@ -164,6 +164,11 @@ export interface FetchMoreQueryOptions { context?: DefaultContext; } +/** + * @deprecated `UpdateQueryFn` is deprecated and will be removed or updated in a + * future version of Apollo Client. Use `SubscribeToMoreUpdateQueryFn` instead + * which provides a more type-safe result. + */ export type UpdateQueryFn< TData = any, TSubscriptionVariables = OperationVariables, @@ -176,19 +181,94 @@ export type UpdateQueryFn< } ) => Unmasked; -export type SubscribeToMoreOptions< +export type UpdateQueryOptions = { + variables?: TVariables; +} & ( + | { + /** + * Indicate if the previous query result has been found fully in the cache. + */ + complete: true; + previousData: Unmasked; + } + | { + /** + * Indicate if the previous query result has not been found fully in the cache. + * Might have partial or missing data. + */ + complete: false; + previousData: DeepPartial> | undefined; + } +); + +export interface UpdateQueryMapFn< TData = any, - TSubscriptionVariables = OperationVariables, + TVariables = OperationVariables, +> { + ( + /** + * @deprecated This value is not type-safe and may contain partial data. This + * argument will be removed in the next major version of Apollo Client. Use + * `options.previousData` instead for a more type-safe value. + */ + unsafePreviousData: Unmasked, + options: UpdateQueryOptions + ): Unmasked | void; +} + +export type SubscribeToMoreUpdateQueryFn< + TData = any, + TVariables extends OperationVariables = OperationVariables, TSubscriptionData = TData, > = { + ( + /** + * @deprecated This value is not type-safe and may contain partial data. This + * argument will be removed in the next major version of Apollo Client. Use + * `options.previousData` instead for a more type-safe value. + */ + unsafePreviousData: Unmasked, + options: UpdateQueryOptions & { + subscriptionData: { data: Unmasked }; + } + ): Unmasked | void; +}; + +export interface SubscribeToMoreOptions< + TData = any, + TSubscriptionVariables extends OperationVariables = OperationVariables, + TSubscriptionData = TData, + TVariables extends OperationVariables = TSubscriptionVariables, +> { document: | DocumentNode | TypedDocumentNode; variables?: TSubscriptionVariables; - updateQuery?: UpdateQueryFn; + updateQuery?: SubscribeToMoreUpdateQueryFn< + TData, + TVariables, + TSubscriptionData + >; onError?: (error: Error) => void; context?: DefaultContext; -}; +} + +export interface SubscribeToMoreFunction< + TData, + TVariables extends OperationVariables = OperationVariables, +> { + < + TSubscriptionData = TData, + TSubscriptionVariables extends OperationVariables = TVariables, + >( + options: SubscribeToMoreOptions< + TData, + TSubscriptionVariables, + TSubscriptionData, + TVariables + > + ): () => void; +} export interface SubscriptionOptions< TVariables = OperationVariables, diff --git a/src/link/retry/__tests__/retryLink.ts b/src/link/retry/__tests__/retryLink.ts index 85955021588..973b8ab92e6 100644 --- a/src/link/retry/__tests__/retryLink.ts +++ b/src/link/retry/__tests__/retryLink.ts @@ -5,7 +5,11 @@ import { execute } from "../../core/execute"; import { Observable } from "../../../utilities/observables/Observable"; import { fromError } from "../../utils/fromError"; import { RetryLink } from "../retryLink"; -import { ObservableStream } from "../../../testing/internal"; +import { + mockMultipartSubscriptionStream, + ObservableStream, +} from "../../../testing/internal"; +import { ApolloError } from "../../../core"; const query = gql` { @@ -210,4 +214,64 @@ describe("RetryLink", () => { [3, operation, standardError], ]); }); + + it("handles protocol errors from multipart subscriptions", async () => { + const subscription = gql` + subscription MySubscription { + aNewDieWasCreated { + die { + roll + sides + color + } + } + } + `; + + const attemptStub = jest.fn(); + attemptStub.mockReturnValueOnce(true); + + const retryLink = new RetryLink({ + delay: { initial: 1 }, + attempts: attemptStub, + }); + + const { httpLink, enqueuePayloadResult, enqueueProtocolErrors } = + mockMultipartSubscriptionStream(); + const link = ApolloLink.from([retryLink, httpLink]); + const stream = new ObservableStream(execute(link, { query: subscription })); + + enqueueProtocolErrors([ + { message: "Error field", extensions: { code: "INTERNAL_SERVER_ERROR" } }, + ]); + + enqueuePayloadResult({ + data: { + aNewDieWasCreated: { die: { color: "blue", roll: 2, sides: 6 } }, + }, + }); + + await expect(stream).toEmitValue({ + data: { + aNewDieWasCreated: { die: { color: "blue", roll: 2, sides: 6 } }, + }, + }); + + expect(attemptStub).toHaveBeenCalledTimes(1); + expect(attemptStub).toHaveBeenCalledWith( + 1, + expect.objectContaining({ + operationName: "MySubscription", + query: subscription, + }), + new ApolloError({ + protocolErrors: [ + { + message: "Error field", + extensions: { code: "INTERNAL_SERVER_ERROR" }, + }, + ], + }) + ); + }); }); diff --git a/src/link/retry/retryLink.ts b/src/link/retry/retryLink.ts index cde2dd2ea9c..37c293d99df 100644 --- a/src/link/retry/retryLink.ts +++ b/src/link/retry/retryLink.ts @@ -7,6 +7,11 @@ import { buildDelayFunction } from "./delayFunction.js"; import type { RetryFunction, RetryFunctionOptions } from "./retryFunction.js"; import { buildRetryFunction } from "./retryFunction.js"; import type { SubscriptionObserver } from "zen-observable-ts"; +import { + ApolloError, + graphQLResultHasProtocolErrors, + PROTOCOL_ERRORS_SYMBOL, +} from "../../errors/index.js"; export namespace RetryLink { export interface Options { @@ -54,7 +59,21 @@ class RetryableOperation { private try() { this.currentSubscription = this.forward(this.operation).subscribe({ - next: this.observer.next.bind(this.observer), + next: (result) => { + if (graphQLResultHasProtocolErrors(result)) { + this.onError( + new ApolloError({ + protocolErrors: result.extensions[PROTOCOL_ERRORS_SYMBOL], + }) + ); + // Unsubscribe from the current subscription to prevent the `complete` + // handler to be called as a result of the stream closing. + this.currentSubscription?.unsubscribe(); + return; + } + + this.observer.next(result); + }, error: this.onError, complete: this.observer.complete.bind(this.observer), }); diff --git a/src/react/hoc/__tests__/queries/lifecycle.test.tsx b/src/react/hoc/__tests__/queries/lifecycle.test.tsx index 76134ba081c..d7b597ae356 100644 --- a/src/react/hoc/__tests__/queries/lifecycle.test.tsx +++ b/src/react/hoc/__tests__/queries/lifecycle.test.tsx @@ -720,6 +720,9 @@ describe("[queries] lifecycle", () => { }); it("handles synchronous racecondition with prefilled data from the server", async () => { + using _act = disableActEnvironment(); + const renderStream = createRenderStream>(); + const query: DocumentNode = gql` query GetUser($first: Int) { user(first: $first) { @@ -754,36 +757,40 @@ describe("[queries] lifecycle", () => { cache: new Cache({ addTypename: false }).restore(initialState), }); - let count = 0; - let done = false; const Container = graphql(query)( class extends React.Component> { - componentDidMount() { - void this.props.data!.refetch().then((result) => { - expect(result.data!.user.name).toBe("Luke Skywalker"); - done = true; - }); - } - render() { - count++; - const user = this.props.data!.user; - const name = user ? user.name : ""; - if (count === 2) { - expect(name).toBe("Luke Skywalker"); - } + renderStream.replaceSnapshot(this.props); return null; } } ); - render( + await renderStream.render( ); + { + await renderStream.takeRender(); + } + const done = renderStream + .getCurrentRender() + .snapshot.data!.refetch() + .then((result) => { + expect(result.data!.user.name).toBe("Luke Skywalker"); + }); + + { + const { snapshot } = await renderStream.takeRender(); + + const user = snapshot.data!.user; + const name = user ? user.name : ""; + + expect(name).toBe("Luke Skywalker"); + } - await waitFor(() => expect(done).toBeTruthy()); + await done; }); it("handles asynchronous racecondition with prefilled data from the server", async () => { diff --git a/src/react/hoc/types.ts b/src/react/hoc/types.ts index 0d67f4c1ca0..892727c0db7 100644 --- a/src/react/hoc/types.ts +++ b/src/react/hoc/types.ts @@ -1,13 +1,14 @@ -import type { ApolloCache, ApolloClient } from "../../core/index.js"; import type { ApolloError } from "../../errors/index.js"; import type { + ApolloCache, + ApolloClient, ApolloQueryResult, - OperationVariables, + DefaultContext, FetchMoreOptions, - UpdateQueryOptions, FetchMoreQueryOptions, + OperationVariables, SubscribeToMoreOptions, - DefaultContext, + UpdateQueryMapFn, } from "../../core/index.js"; import type { MutationFunction, @@ -32,9 +33,7 @@ export interface QueryControls< startPolling: (pollInterval: number) => void; stopPolling: () => void; subscribeToMore: (options: SubscribeToMoreOptions) => () => void; - updateQuery: ( - mapFn: (previousQueryResult: any, options: UpdateQueryOptions) => any - ) => void; + updateQuery: (mapFn: UpdateQueryMapFn) => void; } export type DataValue< diff --git a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx index a335ede0877..5e788e03b56 100644 --- a/src/react/hooks/__tests__/useBackgroundQuery.test.tsx +++ b/src/react/hooks/__tests__/useBackgroundQuery.test.tsx @@ -42,6 +42,7 @@ import equal from "@wry/equality"; import { RefetchWritePolicy, SubscribeToMoreOptions, + SubscribeToMoreFunction, } from "../../../core/watchQueryOptions"; import { skipToken } from "../constants"; import { @@ -57,7 +58,6 @@ import { spyOnConsole, addDelayToMocks, } from "../../../testing/internal"; -import { SubscribeToMoreFunction } from "../useSuspenseQuery"; import { MaskedVariablesCaseData, setupMaskedVariablesCase, @@ -7190,6 +7190,8 @@ describe("fetchMore", () => { expect(updateQuery).toHaveBeenCalledWith( { greeting: "Hello" }, { + complete: true, + previousData: { greeting: "Hello" }, subscriptionData: { data: { greetingUpdated: "Subscription hello" }, }, @@ -8364,9 +8366,12 @@ describe.skip("type tests", () => { { const [, { subscribeToMore }] = useBackgroundQuery(query); - const subscription: MaskedDocumentNode = gql` - subscription { - pushLetter { + const subscription: MaskedDocumentNode< + Subscription, + { letterId: string } + > = gql` + subscription LetterPushed($letterId: ID!) { + pushLetter(letterId: $letterId) { id ...CharacterFragment } @@ -8379,15 +8384,41 @@ describe.skip("type tests", () => { subscribeToMore({ document: subscription, - updateQuery: (queryData, { subscriptionData }) => { + updateQuery: ( + queryData, + { subscriptionData, variables, complete, previousData } + ) => { expectTypeOf(queryData).toEqualTypeOf(); expectTypeOf(queryData).not.toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + | UnmaskedVariablesCaseData + | DeepPartial + | undefined + >(); + + if (complete) { + // Should narrow the type + expectTypeOf( + previousData + ).toEqualTypeOf(); + expectTypeOf( + previousData + ).not.toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } expectTypeOf( subscriptionData.data ).toEqualTypeOf(); expectTypeOf(subscriptionData.data).not.toEqualTypeOf(); + expectTypeOf(variables).toEqualTypeOf< + VariablesCaseVariables | undefined + >(); + return {} as UnmaskedVariablesCaseData; }, }); @@ -8411,16 +8442,43 @@ describe.skip("type tests", () => { subscribeToMore({ document: subscription, - updateQuery: (queryData, { subscriptionData }) => { + updateQuery: ( + queryData, + { subscriptionData, variables, complete, previousData } + ) => { expectTypeOf(queryData).toEqualTypeOf(); expectTypeOf(queryData).not.toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + | UnmaskedVariablesCaseData + | DeepPartial + | undefined + >(); + + if (complete) { + // Should narrow the type + expectTypeOf( + previousData + ).toEqualTypeOf(); + expectTypeOf( + previousData + ).not.toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + expectTypeOf( subscriptionData.data ).toEqualTypeOf(); expectTypeOf(subscriptionData.data).not.toEqualTypeOf(); - return {} as UnmaskedVariablesCaseData; + expectTypeOf(variables).toEqualTypeOf< + VariablesCaseVariables | undefined + >(); + + return queryData; }, }); } diff --git a/src/react/hooks/__tests__/useLazyQuery.test.tsx b/src/react/hooks/__tests__/useLazyQuery.test.tsx index 802638fef98..c9e57c608b0 100644 --- a/src/react/hooks/__tests__/useLazyQuery.test.tsx +++ b/src/react/hooks/__tests__/useLazyQuery.test.tsx @@ -12,7 +12,7 @@ import { NetworkStatus, TypedDocumentNode, } from "../../../core"; -import { Observable } from "../../../utilities"; +import { DeepPartial, Observable } from "../../../utilities"; import { ApolloProvider } from "../../../react"; import { MockedProvider, @@ -1533,6 +1533,8 @@ describe("useLazyQuery Hook", () => { variables: {}, }); } + + await expect(takeSnapshot).not.toRerender(); }); it("the promise should not cause an unhandled rejection", async () => { @@ -3351,8 +3353,20 @@ describe.skip("Type Tests", () => { subscribeToMore({ document: gql`` as TypedDocumentNode, - updateQuery(queryData, { subscriptionData }) { + updateQuery(queryData, { subscriptionData, complete, previousData }) { expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf(complete).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + UnmaskedQuery | DeepPartial | undefined + >(); + + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } expectTypeOf( subscriptionData.data ).toEqualTypeOf(); @@ -3361,8 +3375,12 @@ describe.skip("Type Tests", () => { }, }); - updateQuery((previousData) => { - expectTypeOf(previousData).toEqualTypeOf(); + updateQuery((_previousData, { complete, previousData }) => { + expectTypeOf(_previousData).toEqualTypeOf(); + expectTypeOf(complete).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + UnmaskedQuery | DeepPartial | undefined + >(); return {} as UnmaskedQuery; }); @@ -3450,20 +3468,41 @@ describe.skip("Type Tests", () => { subscribeToMore({ document: gql`` as TypedDocumentNode, - updateQuery(queryData, { subscriptionData }) { + updateQuery(queryData, { subscriptionData, complete, previousData }) { expectTypeOf(queryData).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + UnmaskedQuery | DeepPartial | undefined + >(); expectTypeOf( subscriptionData.data ).toEqualTypeOf(); + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + return {} as UnmaskedQuery; }, }); - updateQuery((previousData) => { - expectTypeOf(previousData).toEqualTypeOf(); - - return {} as UnmaskedQuery; + updateQuery((_previousData, { complete, previousData }) => { + expectTypeOf(_previousData).toEqualTypeOf(); + expectTypeOf(complete).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + UnmaskedQuery | DeepPartial | undefined + >(); + + if (complete) { + expectTypeOf(previousData).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } }); { diff --git a/src/react/hooks/__tests__/useLoadableQuery.test.tsx b/src/react/hooks/__tests__/useLoadableQuery.test.tsx index 86227f9775c..764edd1be8f 100644 --- a/src/react/hooks/__tests__/useLoadableQuery.test.tsx +++ b/src/react/hooks/__tests__/useLoadableQuery.test.tsx @@ -18,6 +18,7 @@ import { SubscribeToMoreOptions, split, } from "../../../core"; +import { SubscribeToMoreFunction } from "../../../core/watchQueryOptions"; import { MockedProvider, MockedProviderProps, @@ -39,11 +40,7 @@ import { ApolloProvider } from "../../context"; import { InMemoryCache } from "../../../cache"; import { LoadableQueryHookFetchPolicy } from "../../types/types"; import { QueryRef } from "../../../react"; -import { - FetchMoreFunction, - RefetchFunction, - SubscribeToMoreFunction, -} from "../useSuspenseQuery"; +import { FetchMoreFunction, RefetchFunction } from "../useSuspenseQuery"; import invariant, { InvariantError } from "ts-invariant"; import { SimpleCaseData, @@ -5097,6 +5094,8 @@ it("can subscribe to subscriptions and react to cache updates via `subscribeToMo expect(updateQuery).toHaveBeenCalledWith( { greeting: "Hello" }, { + complete: true, + previousData: { greeting: "Hello" }, subscriptionData: { data: { greetingUpdated: "Subscription hello" }, }, diff --git a/src/react/hooks/__tests__/useMutation.test.tsx b/src/react/hooks/__tests__/useMutation.test.tsx index 2ac84fb45b8..0433d37b339 100644 --- a/src/react/hooks/__tests__/useMutation.test.tsx +++ b/src/react/hooks/__tests__/useMutation.test.tsx @@ -1206,6 +1206,61 @@ describe("useMutation Hook", () => { expect.objectContaining({ variables }) ); }); + + // https://github.com/apollographql/apollo-client/issues/12008 + it("does not call onError if errors are thrown in the onCompleted callback", async () => { + const CREATE_TODO_DATA = { + createTodo: { + id: 1, + priority: "Low", + description: "Get milk!", + __typename: "Todo", + }, + }; + + const variables = { + priority: "Low", + description: "Get milk2.", + }; + + const mocks = [ + { + request: { + query: CREATE_TODO_MUTATION, + variables, + }, + result: { + data: CREATE_TODO_DATA, + }, + }, + ]; + + const onError = jest.fn(); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useMutation(CREATE_TODO_MUTATION, { + onCompleted: () => { + throw new Error("Oops"); + }, + onError, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + const [createTodo] = await takeSnapshot(); + + await expect(createTodo({ variables })).rejects.toEqual( + new Error("Oops") + ); + + expect(onError).not.toHaveBeenCalled(); + }); }); describe("ROOT_MUTATION cache data", () => { diff --git a/src/react/hooks/__tests__/useQuery.test.tsx b/src/react/hooks/__tests__/useQuery.test.tsx index e71288020c9..c80b89a45eb 100644 --- a/src/react/hooks/__tests__/useQuery.test.tsx +++ b/src/react/hooks/__tests__/useQuery.test.tsx @@ -1620,6 +1620,99 @@ describe("useQuery Hook", () => { await expect(takeSnapshot).not.toRerender(); }); + + // https://github.com/apollographql/apollo-client/issues/12458 + it("returns correct result when cache updates after changing variables and skipping query", async () => { + interface Data { + user: { + __typename: "User"; + id: number; + name: string; + }; + } + + const query: TypedDocumentNode = gql` + query ($id: ID!) { + user(id: $id) { + id + name + } + } + `; + + const client = new ApolloClient({ + link: ApolloLink.empty(), + cache: new InMemoryCache(), + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + ({ id, skip }) => useQuery(query, { skip, variables: { id } }), + { + initialProps: { id: 1, skip: true }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); + + client.writeQuery({ + query, + variables: { id: 1 }, + data: { user: { __typename: "User", id: 1, name: "User 1" } }, + }); + await rerender({ id: 1, skip: false }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { user: { __typename: "User", id: 1, name: "User 1" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: undefined, + variables: { id: 1 }, + }); + + await rerender({ id: 2, skip: true }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: undefined, + error: undefined, + called: false, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { user: { __typename: "User", id: 1, name: "User 1" } }, + variables: { id: 2 }, + }); + + client.writeQuery({ + query, + variables: { id: 2 }, + data: { user: { __typename: "User", id: 2, name: "User 2" } }, + }); + + await rerender({ id: 2, skip: false }); + + await expect(takeSnapshot()).resolves.toEqualQueryResult({ + data: { user: { __typename: "User", id: 2, name: "User 2" } }, + called: true, + loading: false, + networkStatus: NetworkStatus.ready, + previousData: { user: { __typename: "User", id: 1, name: "User 1" } }, + variables: { id: 2 }, + }); + + await expect(takeSnapshot).not.toRerender(); + }); }); describe("options.defaultOptions", () => { @@ -2235,7 +2328,7 @@ describe("useQuery Hook", () => { function checkObservableQueries(expectedLinkCount: number) { const obsQueries = client.getObservableQueries("all"); const { observable } = getCurrentSnapshot(); - expect(obsQueries.size).toBe(2); + expect(obsQueries.size).toBe(IS_REACT_17 || IS_REACT_18 ? 2 : 1); const activeSet = new Set(); const inactiveSet = new Set(); @@ -3971,7 +4064,7 @@ describe("useQuery Hook", () => { errors: [{ message: "error" }], called: true, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, previousData: undefined, variables: {}, }); @@ -4039,7 +4132,7 @@ describe("useQuery Hook", () => { errors: [{ message: 'Could not fetch "hello"' }], called: true, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, previousData: undefined, variables: {}, }); @@ -4103,7 +4196,7 @@ describe("useQuery Hook", () => { errors: [{ message: 'Could not fetch "hello"' }], called: true, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, previousData: undefined, variables: {}, }); @@ -12025,7 +12118,7 @@ describe("useQuery Hook", () => { ], called: true, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, previousData: { hero: { heroFriends: [ @@ -13506,7 +13599,7 @@ describe("useQuery Hook", () => { errors: [{ message: "Couldn't get name" }], called: true, loading: false, - networkStatus: NetworkStatus.ready, + networkStatus: NetworkStatus.error, previousData: undefined, variables: {}, }); diff --git a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx index 48a8ac2ba64..f0db8609db3 100644 --- a/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx +++ b/src/react/hooks/__tests__/useQueryRefHandlers.test.tsx @@ -4,11 +4,14 @@ import { ApolloClient, InMemoryCache, NetworkStatus, - SubscribeToMoreOptions, TypedDocumentNode, gql, split, } from "../../../core"; +import { + SubscribeToMoreFunction, + SubscribeToMoreUpdateQueryFn, +} from "../../../core/watchQueryOptions"; import { MockLink, MockSubscriptionLink, @@ -23,7 +26,6 @@ import { } from "../../../testing/internal"; import { useQueryRefHandlers } from "../useQueryRefHandlers"; import { UseReadQueryResult, useReadQuery } from "../useReadQuery"; -import type { SubscribeToMoreFunction } from "../useSuspenseQuery"; import { Suspense } from "react"; import { createQueryPreloader } from "../../query-preloader/createQueryPreloader"; import userEvent from "@testing-library/user-event"; @@ -1960,12 +1962,10 @@ test("can subscribe to subscriptions and react to cache updates via `subscribeTo greetingUpdated: string; } - type UpdateQueryFn = NonNullable< - SubscribeToMoreOptions< - SimpleCaseData, - Record, - SubscriptionData - >["updateQuery"] + type UpdateQueryFn = SubscribeToMoreUpdateQueryFn< + SimpleCaseData, + Record, + SubscriptionData >; const subscription: TypedDocumentNode< @@ -2092,6 +2092,8 @@ test("can subscribe to subscriptions and react to cache updates via `subscribeTo expect(updateQuery).toHaveBeenCalledWith( { greeting: "Hello" }, { + complete: true, + previousData: { greeting: "Hello" }, subscriptionData: { data: { greetingUpdated: "Subscription hello" }, }, diff --git a/src/react/hooks/__tests__/useSuspenseFragment.test.tsx b/src/react/hooks/__tests__/useSuspenseFragment.test.tsx new file mode 100644 index 00000000000..d24d1804348 --- /dev/null +++ b/src/react/hooks/__tests__/useSuspenseFragment.test.tsx @@ -0,0 +1,2007 @@ +import { + useSuspenseFragment, + UseSuspenseFragmentResult, +} from "../useSuspenseFragment"; +import { + ApolloClient, + FragmentType, + gql, + InMemoryCache, + Masked, + MaskedDocumentNode, + MaybeMasked, + OperationVariables, + TypedDocumentNode, +} from "../../../core"; +import React, { Suspense } from "react"; +import { ApolloProvider } from "../../context"; +import { + createRenderStream, + disableActEnvironment, + renderHookToSnapshotStream, + useTrackRenders, +} from "@testing-library/react-render-stream"; +import { renderAsync, spyOnConsole } from "../../../testing/internal"; +import { act, renderHook, screen, waitFor } from "@testing-library/react"; +import { InvariantError } from "ts-invariant"; +import { MockedProvider, MockSubscriptionLink, wait } from "../../../testing"; +import { expectTypeOf } from "expect-type"; +import userEvent from "@testing-library/user-event"; + +function createDefaultRenderStream() { + return createRenderStream({ + initialSnapshot: { + result: null as UseSuspenseFragmentResult> | null, + }, + }); +} + +function createDefaultTrackedComponents() { + function SuspenseFallback() { + useTrackRenders(); + return
    Loading
    ; + } + + return { SuspenseFallback }; +} + +test("validates the GraphQL document is a fragment", () => { + using _ = spyOnConsole("error"); + + const fragment = gql` + query ShouldThrow { + createException + } + `; + + expect(() => { + renderHook( + () => useSuspenseFragment({ fragment, from: { __typename: "Nope" } }), + { wrapper: ({ children }) => {children} } + ); + }).toThrow( + new InvariantError( + "Found a query operation named 'ShouldThrow'. No operations are allowed when using a fragment as a query. Only fragments are allowed." + ) + ); +}); + +test("throws if no client is provided", () => { + using _spy = spyOnConsole("error"); + expect(() => + renderHook(() => + useSuspenseFragment({ + fragment: gql` + fragment ShouldThrow on Error { + shouldThrow + } + `, + from: {}, + }) + ) + ).toThrow(/pass an ApolloClient/); +}); + +test("suspends until cache value is complete", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const { render, takeRender, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + function App() { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => { + return {children}; + }, + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("updates when the cache updates", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const { takeRender, render, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + function App() { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("resuspends when data goes missing until complete again", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const { takeRender, render, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + function App() { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + client.cache.modify({ + id: "Item:1", + fields: { + text: (_, { DELETE }) => DELETE, + }, + }); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("does not suspend and returns cache data when data is already in the cache", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const { takeRender, render, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Cached" }, + }); + + function App() { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Cached", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("receives cache updates after initial result when data is written to the cache before mounted", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const { takeRender, render, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Cached" }, + }); + + function App() { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Cached", + }, + }); + } + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Updated" }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Updated", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("allows the client to be overridden", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const defaultClient = new ApolloClient({ cache: new InMemoryCache() }); + const client = new ApolloClient({ cache: new InMemoryCache() }); + + defaultClient.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Should not be used" }, + }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ + fragment, + client, + from: { __typename: "Item", id: 1 }, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + const { data } = await takeSnapshot(); + + expect(data).toEqual({ __typename: "Item", id: 1, text: "Item #1" }); +}); + +test("suspends until data is complete when changing `from` with no data written to cache", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const { takeRender, replaceSnapshot, render } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + using _disabledAct = disableActEnvironment(); + function App({ id }: { id: number }) { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id }, + }); + + replaceSnapshot({ result }); + + return null; + } + + const { rerender } = await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + await rerender( + }> + + + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 2, text: "Item #2" }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 2, + text: "Item #2", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("does not suspend when changing `from` with data already written to cache", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const { takeRender, replaceSnapshot, render } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 2, text: "Item #2" }, + }); + + using _disabledAct = disableActEnvironment(); + function App({ id }: { id: number }) { + useTrackRenders(); + + const result = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id }, + }); + + replaceSnapshot({ result }); + + return null; + } + + const { rerender } = await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + await rerender( + }> + + + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 2, + text: "Item #2", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +it("does not rerender when fields with @nonreactive change", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text @nonreactive + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + using _disabledAct = disableActEnvironment(); + + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ fragment, from: { __typename: "Item", id: 1 } }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ + __typename: "Item", + id: 1, + text: "Item #1", + }); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +it("does not rerender when fields with @nonreactive on nested fragment change", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + ...ItemFields @nonreactive + } + + fragment ItemFields on Item { + text + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + fragmentName: "ItemFragment", + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + using _disabledAct = disableActEnvironment(); + + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ + fragment, + fragmentName: "ItemFragment", + from: { __typename: "Item", id: 1 }, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ + __typename: "Item", + id: 1, + text: "Item #1", + }); + } + + client.writeFragment({ + fragment, + fragmentName: "ItemFragment", + data: { + __typename: "Item", + id: 1, + text: "Item #1 (updated)", + }, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +// TODO: Update when https://github.com/apollographql/apollo-client/issues/12003 is fixed +it.failing( + "warns and suspends when passing parent object to `from` when key fields are missing", + async () => { + using _ = spyOnConsole("warn"); + + interface Fragment { + age: number; + } + + const fragment: TypedDocumentNode = gql` + fragment UserFields on User { + age + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const { replaceSnapshot, render, takeRender } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + function App() { + const result = useSuspenseFragment({ + fragment, + from: { __typename: "User" }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + expect(console.warn).toHaveBeenCalledTimes(1); + expect(console.warn).toHaveBeenCalledWith( + "Could not identify object passed to `from` for '%s' fragment, either because the object is non-normalized or the key fields are missing. If you are masking this object, please ensure the key fields are requested by the parent object.", + "UserFields" + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + } +); + +test("returns null if `from` is `null`", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => useSuspenseFragment({ fragment, from: null }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + const { data } = await takeSnapshot(); + + expect(data).toBeNull(); +}); + +test("returns cached value when `from` changes from `null` to non-null value", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + ({ id }) => + useSuspenseFragment({ + fragment, + from: id === null ? null : { __typename: "Item", id }, + }), + { + initialProps: { id: null as null | number }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toBeNull(); + } + + await rerender({ id: 1 }); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ + __typename: "Item", + id: 1, + text: "Item #1", + }); + } + + await expect(takeSnapshot).not.toRerender(); +}); + +test("returns null value when `from` changes from non-null value to `null`", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot, rerender } = await renderHookToSnapshotStream( + ({ id }) => + useSuspenseFragment({ + fragment, + from: id === null ? null : { __typename: "Item", id }, + }), + { + initialProps: { id: 1 as null | number }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ + __typename: "Item", + id: 1, + text: "Item #1", + }); + } + + await rerender({ id: null }); + + { + const { data } = await takeSnapshot(); + + expect(data).toBeNull(); + } + + await expect(takeSnapshot).not.toRerender(); +}); + +test("suspends until cached value is available when `from` changes from `null` to non-null value", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const client = new ApolloClient({ cache: new InMemoryCache() }); + + const { takeRender, render, replaceSnapshot } = + createDefaultRenderStream(); + const { SuspenseFallback } = createDefaultTrackedComponents(); + + function App({ id }: { id: number | null }) { + useTrackRenders(); + const result = useSuspenseFragment({ + fragment, + from: id === null ? null : { __typename: "Item", id }, + }); + + replaceSnapshot({ result }); + + return null; + } + + using _disabledAct = disableActEnvironment(); + const { rerender } = await render( + }> + + , + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ data: null }); + } + + await rerender( + }> + + + ); + + { + const { renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([SuspenseFallback]); + } + + client.writeFragment({ + fragment, + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([App]); + expect(snapshot.result).toEqual({ + data: { + __typename: "Item", + id: 1, + text: "Item #1", + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("returns masked fragment when data masking is enabled", async () => { + type Post = { + __typename: "Post"; + id: number; + title: string; + } & { " $fragmentRefs"?: { PostFields: PostFields } }; + + type PostFields = { + __typename: "Post"; + updatedAt: string; + } & { " $fragmentName"?: "PostFields" }; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + const fragment: TypedDocumentNode = gql` + fragment PostFragment on Post { + id + title + ...PostFields + } + + fragment PostFields on Post { + updatedAt + } + `; + + client.writeFragment({ + fragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + updatedAt: "2024-01-01", + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ + fragment, + fragmentName: "PostFragment", + from: { __typename: "Post", id: 1 }, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const snapshot = await takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }); + } + + await expect(takeSnapshot).not.toRerender(); +}); + +test("does not rerender for cache writes to masked fields", async () => { + type Post = { + __typename: "Post"; + id: number; + title: string; + } & { " $fragmentRefs"?: { PostFields: PostFields } }; + + type PostFields = { + __typename: "Post"; + updatedAt: string; + } & { " $fragmentName"?: "PostFields" }; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + const fragment: TypedDocumentNode = gql` + fragment PostFragment on Post { + id + title + ...PostFields + } + + fragment PostFields on Post { + updatedAt + } + `; + + client.writeFragment({ + fragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + updatedAt: "2024-01-01", + }, + }); + + using _disabledAct = disableActEnvironment(); + const { takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ + fragment, + fragmentName: "PostFragment", + from: { __typename: "Post", id: 1 }, + }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const snapshot = await takeSnapshot(); + + expect(snapshot).toEqual({ + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }); + } + + client.writeFragment({ + fragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + updatedAt: "2024-02-01", + }, + }); + + await expect(takeSnapshot).not.toRerender(); +}); + +test("updates child fragments for cache updates to masked fields", async () => { + type Post = { + __typename: "Post"; + id: number; + title: string; + } & { " $fragmentRefs"?: { PostFields: PostFields } }; + + type PostFields = { + __typename: "Post"; + updatedAt: string; + } & { " $fragmentName"?: "PostFields" }; + + const client = new ApolloClient({ + dataMasking: true, + cache: new InMemoryCache(), + }); + + const postFieldsFragment: MaskedDocumentNode = gql` + fragment PostFields on Post { + updatedAt + } + `; + + const postFragment: MaskedDocumentNode = gql` + fragment PostFragment on Post { + id + title + ...PostFields + } + + ${postFieldsFragment} + `; + + client.writeFragment({ + fragment: postFragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + updatedAt: "2024-01-01", + }, + }); + + const { render, mergeSnapshot, takeRender } = createRenderStream({ + initialSnapshot: { + parent: null as UseSuspenseFragmentResult> | null, + child: null as UseSuspenseFragmentResult> | null, + }, + }); + + function Parent() { + useTrackRenders(); + const parent = useSuspenseFragment({ + fragment: postFragment, + fragmentName: "PostFragment", + from: { __typename: "Post", id: 1 }, + }); + + mergeSnapshot({ parent }); + + return ; + } + + function Child({ post }: { post: FragmentType }) { + useTrackRenders(); + const child = useSuspenseFragment({ + fragment: postFieldsFragment, + from: post, + }); + + mergeSnapshot({ child }); + return null; + } + + using _disabledAct = disableActEnvironment(); + await render(, { + wrapper: ({ children }) => ( + {children} + ), + }); + + { + const { snapshot } = await takeRender(); + + expect(snapshot).toEqual({ + parent: { + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }, + child: { + data: { + __typename: "Post", + updatedAt: "2024-01-01", + }, + }, + }); + } + + client.writeFragment({ + fragment: postFragment, + fragmentName: "PostFragment", + data: { + __typename: "Post", + id: 1, + title: "Blog post", + updatedAt: "2024-02-01", + }, + }); + + { + const { snapshot, renderedComponents } = await takeRender(); + + expect(renderedComponents).toStrictEqual([Child]); + expect(snapshot).toEqual({ + parent: { + data: { + __typename: "Post", + id: 1, + title: "Blog post", + }, + }, + child: { + data: { + __typename: "Post", + updatedAt: "2024-02-01", + }, + }, + }); + } + + await expect(takeRender).not.toRerender(); +}); + +test("tears down the subscription on unmount", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ cache }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + using _disabledAct = disableActEnvironment(); + const { unmount, takeSnapshot } = await renderHookToSnapshotStream( + () => + useSuspenseFragment({ fragment, from: { __typename: "Item", id: 1 } }), + { + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ __typename: "Item", id: 1, text: "Item #1" }); + } + + expect(cache["watches"].size).toBe(1); + + unmount(); + // We need to wait a tick since the cleanup is run in a setTimeout to + // prevent strict mode bugs. + await wait(0); + + expect(cache["watches"].size).toBe(0); +}); + +test("tears down all watches when rendering multiple records", async () => { + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const cache = new InMemoryCache(); + const client = new ApolloClient({ cache }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 2, text: "Item #2" }, + }); + + using _disabledAct = disableActEnvironment(); + const { unmount, rerender, takeSnapshot } = await renderHookToSnapshotStream( + ({ id }) => + useSuspenseFragment({ fragment, from: { __typename: "Item", id } }), + { + initialProps: { id: 1 }, + wrapper: ({ children }) => ( + {children} + ), + } + ); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ __typename: "Item", id: 1, text: "Item #1" }); + } + + await rerender({ id: 2 }); + + { + const { data } = await takeSnapshot(); + + expect(data).toEqual({ __typename: "Item", id: 2, text: "Item #2" }); + } + + unmount(); + // We need to wait a tick since the cleanup is run in a setTimeout to + // prevent strict mode bugs. + await wait(0); + + expect(cache["watches"].size).toBe(0); +}); + +test("tears down watches after default autoDisposeTimeoutMs if component never renders again after suspending", async () => { + jest.useFakeTimers(); + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const cache = new InMemoryCache(); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + const link = new MockSubscriptionLink(); + const client = new ApolloClient({ link, cache }); + + function App() { + const [showItem, setShowItem] = React.useState(true); + + return ( + + + {showItem && ( + + + + )} + + ); + } + + function Item() { + const { data } = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + return {data.text}; + } + + await renderAsync(); + + // Ensure suspends immediately + expect(screen.getByText("Loading item...")).toBeInTheDocument(); + + // Hide the greeting before it finishes loading data + await act(() => user.click(screen.getByText("Hide item"))); + + expect(screen.queryByText("Loading item...")).not.toBeInTheDocument(); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + // clear the microtask queue + await act(() => Promise.resolve()); + + expect(cache["watches"].size).toBe(1); + + jest.advanceTimersByTime(30_000); + + expect(cache["watches"].size).toBe(0); + + jest.useRealTimers(); +}); + +test("tears down watches after configured autoDisposeTimeoutMs if component never renders again after suspending", async () => { + jest.useFakeTimers(); + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + const client = new ApolloClient({ + link, + cache, + defaultOptions: { + react: { + suspense: { + autoDisposeTimeoutMs: 5000, + }, + }, + }, + }); + + function App() { + const [showItem, setShowItem] = React.useState(true); + + return ( + + + {showItem && ( + + + + )} + + ); + } + + function Item() { + const { data } = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + return {data.text}; + } + + await renderAsync(); + + // Ensure suspends immediately + expect(screen.getByText("Loading item...")).toBeInTheDocument(); + + // Hide the greeting before it finishes loading data + await act(() => user.click(screen.getByText("Hide item"))); + + expect(screen.queryByText("Loading item...")).not.toBeInTheDocument(); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + // clear the microtask queue + await act(() => Promise.resolve()); + + expect(cache["watches"].size).toBe(1); + + jest.advanceTimersByTime(5000); + + expect(cache["watches"].size).toBe(0); + + jest.useRealTimers(); +}); + +test("cancels autoDisposeTimeoutMs if the component renders before timer finishes", async () => { + jest.useFakeTimers(); + interface ItemFragment { + __typename: "Item"; + id: number; + text: string; + } + + const fragment: TypedDocumentNode = gql` + fragment ItemFragment on Item { + id + text + } + `; + + const link = new MockSubscriptionLink(); + const cache = new InMemoryCache(); + const client = new ApolloClient({ link, cache }); + + function App() { + return ( + + + + + + ); + } + + function Item() { + const { data } = useSuspenseFragment({ + fragment, + from: { __typename: "Item", id: 1 }, + }); + + return {data.text}; + } + + await renderAsync(); + + // Ensure suspends immediately + expect(screen.getByText("Loading item...")).toBeInTheDocument(); + + client.writeFragment({ + fragment, + data: { __typename: "Item", id: 1, text: "Item #1" }, + }); + + // clear the microtask queue + await act(() => Promise.resolve()); + + await waitFor(() => { + expect(screen.getByText("Item #1")).toBeInTheDocument(); + }); + + jest.advanceTimersByTime(30_000); + + expect(cache["watches"].size).toBe(1); + + jest.useRealTimers(); +}); + +describe.skip("type tests", () => { + test("returns TData when from is a non-null value", () => { + type Data = { foo: string }; + const fragment: TypedDocumentNode = gql``; + + { + const { data } = useSuspenseFragment({ + fragment, + from: { __typename: "Query" }, + }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + + { + const { data } = useSuspenseFragment({ + fragment: gql``, + from: { __typename: "Query" }, + }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + }); + + test("returns null when from is null", () => { + type Data = { foo: string }; + type Vars = Record; + const fragment: TypedDocumentNode = gql``; + + { + const { data } = useSuspenseFragment({ fragment, from: null }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + + { + const { data } = useSuspenseFragment({ + fragment: gql``, + from: null, + }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + }); + + test("returns TData | null when from is nullable", () => { + type Post = { __typename: "Post"; id: number }; + type Vars = Record; + const fragment: TypedDocumentNode = gql``; + const author = {} as { post: Post | null }; + + { + const { data } = useSuspenseFragment({ fragment, from: author.post }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + + { + const { data } = useSuspenseFragment({ + fragment: gql``, + from: author.post, + }); + + expectTypeOf(data).branded.toEqualTypeOf(); + } + }); + + test("variables are optional and can be anything with an untyped DocumentNode", () => { + const fragment = gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { foo: "bar" } }); + useSuspenseFragment({ fragment, from: null, variables: { bar: "baz" } }); + }); + + it("variables are optional and can be anything with unspecified TVariables on a TypedDocumentNode", () => { + const fragment: TypedDocumentNode<{ greeting: string }> = gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { foo: "bar" } }); + useSuspenseFragment({ fragment, from: null, variables: { bar: "baz" } }); + }); + + it("variables are optional and can be anything with OperationVariables on a TypedDocumentNode", () => { + const fragment: TypedDocumentNode< + { greeting: string }, + OperationVariables + > = gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { foo: "bar" } }); + useSuspenseFragment({ fragment, from: null, variables: { bar: "baz" } }); + }); + + it("variables are optional when TVariables are empty", () => { + const fragment: TypedDocumentNode< + { greeting: string }, + Record + > = gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + // @ts-expect-error unknown variable + useSuspenseFragment({ fragment, from: null, variables: { foo: "bar" } }); + }); + + it("does not allow variables when TVariables is `never`", () => { + const fragment: TypedDocumentNode<{ greeting: string }, never> = gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + // @ts-expect-error no variables argument allowed + useSuspenseFragment({ fragment, from: null, variables: { foo: "bar" } }); + }); + + it("optional variables are optional to useSuspenseFragment", () => { + const fragment: TypedDocumentNode<{ posts: string[] }, { limit?: number }> = + gql``; + + useSuspenseFragment({ fragment, from: null }); + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { limit: 10 } }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + limit: 10, + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + it("enforces required variables when TVariables includes required variables", () => { + const fragment: TypedDocumentNode<{ character: string }, { id: string }> = + gql``; + + // @ts-expect-error missing variables argument + useSuspenseFragment({ fragment, from: null }); + // @ts-expect-error empty variables + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { id: "1" } }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); + + it("requires variables with mixed TVariables", () => { + const fragment: TypedDocumentNode< + { character: string }, + { id: string; language?: string } + > = gql``; + + // @ts-expect-error missing variables argument + useSuspenseFragment({ fragment, from: null }); + // @ts-expect-error empty variables + useSuspenseFragment({ fragment, from: null, variables: {} }); + useSuspenseFragment({ fragment, from: null, variables: { id: "1" } }); + useSuspenseFragment({ + fragment, + from: null, + // @ts-expect-error missing required variable + variables: { language: "en" }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { id: "1", language: "en" }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + id: "1", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + useSuspenseFragment({ + fragment, + from: null, + variables: { + id: "1", + language: "en", + // @ts-expect-error unknown variable + foo: "bar", + }, + }); + }); +}); diff --git a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx index b7ba7bc04f1..c2e756ee15f 100644 --- a/src/react/hooks/__tests__/useSuspenseQuery.test.tsx +++ b/src/react/hooks/__tests__/useSuspenseQuery.test.tsx @@ -9636,6 +9636,8 @@ describe("useSuspenseQuery", () => { expect(updateQuery).toHaveBeenCalledWith( { greeting: "Hello" }, { + complete: true, + previousData: { greeting: "Hello" }, subscriptionData: { data: { greetingUpdated: "Subscription hello" }, }, @@ -12939,12 +12941,32 @@ describe("useSuspenseQuery", () => { subscribeToMore({ document: subscription, - updateQuery: (queryData, { subscriptionData }) => { + updateQuery: ( + queryData, + { subscriptionData, complete, previousData } + ) => { expectTypeOf(queryData).toEqualTypeOf(); expectTypeOf( queryData ).not.toEqualTypeOf(); + expectTypeOf(complete).toEqualTypeOf(); + expectTypeOf(previousData).toEqualTypeOf< + | UnmaskedVariablesCaseData + | DeepPartial + | undefined + >(); + + if (complete) { + expectTypeOf( + previousData + ).toEqualTypeOf(); + } else { + expectTypeOf(previousData).toEqualTypeOf< + DeepPartial | undefined + >(); + } + expectTypeOf( subscriptionData.data ).toEqualTypeOf(); diff --git a/src/react/hooks/index.ts b/src/react/hooks/index.ts index 78fc82c61f4..5f0ac41c4b4 100644 --- a/src/react/hooks/index.ts +++ b/src/react/hooks/index.ts @@ -11,6 +11,11 @@ export type { UseSuspenseQueryResult } from "./useSuspenseQuery.js"; export { useSuspenseQuery } from "./useSuspenseQuery.js"; export type { UseBackgroundQueryResult } from "./useBackgroundQuery.js"; export { useBackgroundQuery } from "./useBackgroundQuery.js"; +export type { + UseSuspenseFragmentResult, + UseSuspenseFragmentOptions, +} from "./useSuspenseFragment.js"; +export { useSuspenseFragment } from "./useSuspenseFragment.js"; export type { LoadQueryFunction, UseLoadableQueryResult, diff --git a/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx b/src/react/hooks/internal/__tests__/useRenderGuard.test.tsx index 0f60cb58892..8774e09b5f0 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 "rehackt"; +import * as React from "rehackt"; import { useRenderGuard } from "../useRenderGuard"; import { render, waitFor } from "@testing-library/react"; @@ -23,7 +23,7 @@ it("returns a function that returns `false` if called after render", async () => let result: boolean | typeof UNDEF = UNDEF; function TestComponent() { const calledDuringRender = useRenderGuard(); - useEffect(() => { + React.useEffect(() => { result = calledDuringRender(); }); return <>Test; diff --git a/src/react/hooks/internal/wrapHook.ts b/src/react/hooks/internal/wrapHook.ts index 59b112c3216..175d3e72a5b 100644 --- a/src/react/hooks/internal/wrapHook.ts +++ b/src/react/hooks/internal/wrapHook.ts @@ -5,6 +5,7 @@ import type { useReadQuery, useFragment, useQueryRefHandlers, + useSuspenseFragment, } from "../index.js"; import type { QueryManager } from "../../../core/QueryManager.js"; import type { ApolloClient } from "../../../core/ApolloClient.js"; @@ -17,6 +18,7 @@ interface WrappableHooks { createQueryPreloader: typeof createQueryPreloader; useQuery: typeof useQuery; useSuspenseQuery: typeof useSuspenseQuery; + useSuspenseFragment: typeof useSuspenseFragment; useBackgroundQuery: typeof useBackgroundQuery; useReadQuery: typeof useReadQuery; useFragment: typeof useFragment; diff --git a/src/react/hooks/useBackgroundQuery.ts b/src/react/hooks/useBackgroundQuery.ts index 40480fa0a02..256d3175c90 100644 --- a/src/react/hooks/useBackgroundQuery.ts +++ b/src/react/hooks/useBackgroundQuery.ts @@ -6,6 +6,7 @@ import type { TypedDocumentNode, WatchQueryOptions, } from "../../core/index.js"; +import type { SubscribeToMoreFunction } from "../../core/watchQueryOptions.js"; import { useApolloClient } from "./useApolloClient.js"; import { getSuspenseCache, @@ -17,11 +18,7 @@ import type { CacheKey, QueryRef } from "../internal/index.js"; import type { BackgroundQueryHookOptions, NoInfer } from "../types/types.js"; import { wrapHook } from "./internal/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; -import type { - FetchMoreFunction, - RefetchFunction, - SubscribeToMoreFunction, -} from "./useSuspenseQuery.js"; +import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; import type { DeepPartial } from "../../utilities/index.js"; import type { SkipToken } from "./constants.js"; @@ -293,7 +290,9 @@ function useBackgroundQuery_< { fetchMore, refetch, - subscribeToMore: queryRef.observable.subscribeToMore, + // TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here + subscribeToMore: queryRef.observable + .subscribeToMore as SubscribeToMoreFunction, }, ]; } diff --git a/src/react/hooks/useLazyQuery.ts b/src/react/hooks/useLazyQuery.ts index e22b13fb0d4..3f51b242fc6 100644 --- a/src/react/hooks/useLazyQuery.ts +++ b/src/react/hooks/useLazyQuery.ts @@ -5,6 +5,7 @@ import * as React from "rehackt"; import type { ApolloClient, ApolloQueryResult, + ObservableQuery, OperationVariables, WatchQueryOptions, } from "../../core/index.js"; @@ -17,7 +18,7 @@ import type { QueryHookOptions, QueryResult, } from "../types/types.js"; -import type { InternalResult, ObsQueryWithMeta } from "./useQuery.js"; +import type { InternalResult } from "./useQuery.js"; import { createMakeWatchQueryOptions, getDefaultFetchPolicy, @@ -203,7 +204,7 @@ export function useLazyQuery< function executeQuery( resultData: InternalResult, - observable: ObsQueryWithMeta, + observable: ObservableQuery, client: ApolloClient, currentQuery: DocumentNode, options: QueryHookOptions & { diff --git a/src/react/hooks/useLoadableQuery.ts b/src/react/hooks/useLoadableQuery.ts index b9aa70d11e2..d520b96668c 100644 --- a/src/react/hooks/useLoadableQuery.ts +++ b/src/react/hooks/useLoadableQuery.ts @@ -6,6 +6,10 @@ import type { TypedDocumentNode, WatchQueryOptions, } from "../../core/index.js"; +import type { + SubscribeToMoreFunction, + SubscribeToMoreOptions, +} from "../../core/watchQueryOptions.js"; import { useApolloClient } from "./useApolloClient.js"; import { assertWrappedQueryRef, @@ -18,11 +22,7 @@ import type { CacheKey, QueryRef } from "../internal/index.js"; import type { LoadableQueryHookOptions } from "../types/types.js"; import { __use, useRenderGuard } from "./internal/index.js"; import { useWatchQueryOptions } from "./useSuspenseQuery.js"; -import type { - FetchMoreFunction, - RefetchFunction, - SubscribeToMoreFunction, -} from "./useSuspenseQuery.js"; +import type { FetchMoreFunction, RefetchFunction } from "./useSuspenseQuery.js"; import { canonicalStringify } from "../../cache/index.js"; import type { DeepPartial, @@ -269,7 +269,10 @@ export function useLoadableQuery< "The query has not been loaded. Please load the query." ); - return internalQueryRef.observable.subscribeToMore(options); + return internalQueryRef.observable.subscribeToMore( + // TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here + options as any as SubscribeToMoreOptions + ); }, [internalQueryRef] ); diff --git a/src/react/hooks/useMutation.ts b/src/react/hooks/useMutation.ts index 3c082ed0796..23e51a90b2f 100644 --- a/src/react/hooks/useMutation.ts +++ b/src/react/hooks/useMutation.ts @@ -138,82 +138,87 @@ export function useMutation< return client .mutate(clientOptions as MutationOptions) - .then((response) => { - const { data, errors } = response; - const error = - errors && errors.length > 0 ? - new ApolloError({ graphQLErrors: errors }) - : void 0; - - const onError = - executeOptions.onError || ref.current.options?.onError; - - if (error && onError) { - onError( - error, - clientOptions as MutationOptions - ); - } + .then( + (response) => { + const { data, errors } = response; + const error = + errors && errors.length > 0 ? + new ApolloError({ graphQLErrors: errors }) + : void 0; + + const onError = + executeOptions.onError || ref.current.options?.onError; + + if (error && onError) { + onError( + error, + clientOptions as MutationOptions + ); + } - if ( - mutationId === ref.current.mutationId && - !clientOptions.ignoreResults - ) { - const result = { - called: true, - loading: false, - data, - error, - client, - }; - - if (ref.current.isMounted && !equal(ref.current.result, result)) { - setResult((ref.current.result = result)); + if ( + mutationId === ref.current.mutationId && + !clientOptions.ignoreResults + ) { + const result = { + called: true, + loading: false, + data, + error, + client, + }; + + if (ref.current.isMounted && !equal(ref.current.result, result)) { + setResult((ref.current.result = result)); + } } - } - const onCompleted = - executeOptions.onCompleted || ref.current.options?.onCompleted; + const onCompleted = + executeOptions.onCompleted || ref.current.options?.onCompleted; - if (!error) { - onCompleted?.( - response.data!, - clientOptions as MutationOptions - ); - } + if (!error) { + onCompleted?.( + response.data!, + clientOptions as MutationOptions + ); + } - return response; - }) - .catch((error) => { - if (mutationId === ref.current.mutationId && ref.current.isMounted) { - const result = { - loading: false, - error, - data: void 0, - called: true, - client, - }; - - if (!equal(ref.current.result, result)) { - setResult((ref.current.result = result)); + return response; + }, + (error) => { + if ( + mutationId === ref.current.mutationId && + ref.current.isMounted + ) { + const result = { + loading: false, + error, + data: void 0, + called: true, + client, + }; + + if (!equal(ref.current.result, result)) { + setResult((ref.current.result = result)); + } } - } - const onError = - executeOptions.onError || ref.current.options?.onError; + const onError = + executeOptions.onError || ref.current.options?.onError; - if (onError) { - onError( - error, - clientOptions as MutationOptions - ); + if (onError) { + onError( + error, + clientOptions as MutationOptions + ); - // TODO(brian): why are we returning this here??? - return { data: void 0, errors: error }; - } + // TODO(brian): why are we returning this here??? + return { data: void 0, errors: error }; + } - throw error; - }); + throw error; + } + ); }, [] ); diff --git a/src/react/hooks/useQuery.ts b/src/react/hooks/useQuery.ts index b608c4b8b6b..55d1637dbc3 100644 --- a/src/react/hooks/useQuery.ts +++ b/src/react/hooks/useQuery.ts @@ -23,23 +23,20 @@ import * as React from "rehackt"; import { useSyncExternalStore } from "./useSyncExternalStore.js"; import { equal } from "@wry/equality"; +import { mergeOptions } from "../../utilities/index.js"; +import { getApolloContext } from "../context/index.js"; +import { ApolloError } from "../../errors/index.js"; import type { ApolloClient, DefaultOptions, OperationVariables, WatchQueryFetchPolicy, -} from "../../core/index.js"; -import { mergeOptions } from "../../utilities/index.js"; -import { getApolloContext } from "../context/index.js"; -import { ApolloError } from "../../errors/index.js"; -import type { ApolloQueryResult, - ObservableQuery, DocumentNode, TypedDocumentNode, WatchQueryOptions, } from "../../core/index.js"; -import { NetworkStatus } from "../../core/index.js"; +import { ObservableQuery, NetworkStatus } from "../../core/index.js"; import type { QueryHookOptions, QueryResult, @@ -68,9 +65,9 @@ type InternalQueryResult = Omit< >; function noop() {} -export const lastWatchOptions = Symbol(); +const lastWatchOptions = Symbol(); -export interface ObsQueryWithMeta +interface ObsQueryWithMeta extends ObservableQuery { [lastWatchOptions]?: WatchQueryOptions; } @@ -190,8 +187,10 @@ function useInternalState< // to fetch the result set. This is used during SSR. (renderPromises && renderPromises.getSSRObservable(makeWatchQueryOptions())) || - client.watchQuery( - getObsQueryOptions(void 0, client, options, makeWatchQueryOptions()) + ObservableQuery["inactiveOnCreation"].withValue(!renderPromises, () => + client.watchQuery( + getObsQueryOptions(void 0, client, options, makeWatchQueryOptions()) + ) ), resultData: { // Reuse previousData from previous InternalState (if any) to provide @@ -289,9 +288,10 @@ export function useQueryInternals< watchQueryOptions ); - const obsQueryFields = React.useMemo< - Omit, "variables"> - >(() => bindObservableMethods(observable), [observable]); + const obsQueryFields = React.useMemo( + () => bindObservableMethods(observable), + [observable] + ); useRegisterSSRObservable(observable, renderPromises, ssrAllowed); @@ -825,7 +825,7 @@ const skipStandbyResult = maybeDeepFreeze({ function bindObservableMethods( observable: ObservableQuery -) { +): Omit, "variables"> { return { refetch: observable.refetch.bind(observable), reobserve: observable.reobserve.bind(observable), diff --git a/src/react/hooks/useQueryRefHandlers.ts b/src/react/hooks/useQueryRefHandlers.ts index 8204e3e5df7..b4020196b24 100644 --- a/src/react/hooks/useQueryRefHandlers.ts +++ b/src/react/hooks/useQueryRefHandlers.ts @@ -8,14 +8,13 @@ import { } from "../internal/index.js"; import type { QueryRef } from "../internal/index.js"; import type { OperationVariables } from "../../core/types.js"; -import type { - RefetchFunction, - FetchMoreFunction, - SubscribeToMoreFunction, -} from "./useSuspenseQuery.js"; +import type { SubscribeToMoreFunction } from "../../core/watchQueryOptions.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"; +import type { ApolloClient } from "../../core/ApolloClient.js"; +import type { ObservableQuery } from "../../core/ObservableQuery.js"; export interface UseQueryRefHandlersResult< TData = unknown, @@ -55,21 +54,19 @@ export function useQueryRefHandlers< queryRef: QueryRef ): UseQueryRefHandlersResult { const unwrapped = unwrapQueryRef(queryRef); + const clientOrObsQuery = useApolloClient( + unwrapped ? + // passing an `ObservableQuery` is not supported by the types, but it will + // return any truthy value that is passed in as an override so we cast the result + (unwrapped["observable"] as any) + : undefined + ) as ApolloClient | ObservableQuery; return wrapHook( "useQueryRefHandlers", + // eslint-disable-next-line react-compiler/react-compiler 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 - // This is just a context read - it's fine to do this conditionally. - // This hook wrapper also shouldn't be optimized by React Compiler. - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/rules-of-hooks - : useApolloClient() + clientOrObsQuery )(queryRef); } @@ -121,6 +118,8 @@ function useQueryRefHandlers_< return { refetch, fetchMore, - subscribeToMore: internalQueryRef.observable.subscribeToMore, + // TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here + subscribeToMore: internalQueryRef.observable + .subscribeToMore as SubscribeToMoreFunction, }; } diff --git a/src/react/hooks/useReadQuery.ts b/src/react/hooks/useReadQuery.ts index 5c444e0eb7c..40e3f02e0d0 100644 --- a/src/react/hooks/useReadQuery.ts +++ b/src/react/hooks/useReadQuery.ts @@ -10,7 +10,11 @@ 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 type { + ApolloClient, + NetworkStatus, + ObservableQuery, +} from "../../core/index.js"; import { useApolloClient } from "./useApolloClient.js"; import type { MaybeMasked } from "../../masking/index.js"; @@ -43,21 +47,19 @@ export function useReadQuery( queryRef: QueryRef ): UseReadQueryResult { const unwrapped = unwrapQueryRef(queryRef); + const clientOrObsQuery = useApolloClient( + unwrapped ? + // passing an `ObservableQuery` is not supported by the types, but it will + // return any truthy value that is passed in as an override so we cast the result + (unwrapped["observable"] as any) + : undefined + ) as ApolloClient | ObservableQuery; return wrapHook( "useReadQuery", + // eslint-disable-next-line react-compiler/react-compiler useReadQuery_, - 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 - // This is just a context read - it's fine to do this conditionally. - // This hook wrapper also shouldn't be optimized by React Compiler. - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/rules-of-hooks - : useApolloClient() + clientOrObsQuery )(queryRef); } diff --git a/src/react/hooks/useSuspenseFragment.ts b/src/react/hooks/useSuspenseFragment.ts new file mode 100644 index 00000000000..5aee9e23ac1 --- /dev/null +++ b/src/react/hooks/useSuspenseFragment.ts @@ -0,0 +1,177 @@ +import type { + ApolloClient, + DocumentNode, + OperationVariables, + Reference, + StoreObject, + TypedDocumentNode, +} from "../../core/index.js"; +import { canonicalStringify } from "../../cache/index.js"; +import { useApolloClient } from "./useApolloClient.js"; +import { getSuspenseCache } from "../internal/index.js"; +import * as React from "rehackt"; +import type { FragmentKey } from "../internal/cache/types.js"; +import { __use } from "./internal/__use.js"; +import { wrapHook } from "./internal/index.js"; +import type { FragmentType, MaybeMasked } from "../../masking/index.js"; +import type { NoInfer, VariablesOption } from "../types/types.js"; + +type From = + | StoreObject + | Reference + | FragmentType> + | string + | null; + +export type UseSuspenseFragmentOptions< + TData, + TVariables extends OperationVariables, +> = { + /** + * A GraphQL document created using the `gql` template string tag from + * `graphql-tag` with one or more fragments which will be used to determine + * the shape of data to read. If you provide more than one fragment in this + * document then you must also specify `fragmentName` to select a single. + */ + fragment: DocumentNode | TypedDocumentNode; + + /** + * The name of the fragment in your GraphQL document to be used. If you do + * not provide a `fragmentName` and there is only one fragment in your + * `fragment` document then that fragment will be used. + */ + fragmentName?: string; + from: From; + // Override this field to make it optional (default: true). + optimistic?: boolean; + /** + * The instance of `ApolloClient` to use to look up the fragment. + * + * By default, the instance that's passed down via context is used, but you + * can provide a different instance here. + * + * @docGroup 1. Operation options + */ + client?: ApolloClient; +} & VariablesOption>; + +export type UseSuspenseFragmentResult = { data: MaybeMasked }; + +const NULL_PLACEHOLDER = [] as unknown as [ + FragmentKey, + Promise | null>, +]; + +export function useSuspenseFragment< + TData, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions & { + from: NonNullable>; + } +): UseSuspenseFragmentResult; + +export function useSuspenseFragment< + TData, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions & { + from: null; + } +): UseSuspenseFragmentResult; + +export function useSuspenseFragment< + TData, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions & { + from: From; + } +): UseSuspenseFragmentResult; + +export function useSuspenseFragment< + TData, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions +): UseSuspenseFragmentResult; + +export function useSuspenseFragment< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions +): UseSuspenseFragmentResult { + return wrapHook( + "useSuspenseFragment", + // eslint-disable-next-line react-compiler/react-compiler + useSuspenseFragment_, + useApolloClient(typeof options === "object" ? options.client : undefined) + )(options); +} + +function useSuspenseFragment_< + TData = unknown, + TVariables extends OperationVariables = OperationVariables, +>( + options: UseSuspenseFragmentOptions +): UseSuspenseFragmentResult { + const client = useApolloClient(options.client); + const { from, variables } = options; + const { cache } = client; + + const id = React.useMemo( + () => + typeof from === "string" ? from + : from === null ? null + : cache.identify(from), + [cache, from] + ) as string | null; + + const fragmentRef = + id === null ? null : ( + getSuspenseCache(client).getFragmentRef( + [id, options.fragment, canonicalStringify(variables)], + client, + { ...options, variables: variables as TVariables, from: id } + ) + ); + + let [current, setPromise] = React.useState< + [FragmentKey, Promise | null>] + >( + fragmentRef === null ? NULL_PLACEHOLDER : ( + [fragmentRef.key, fragmentRef.promise] + ) + ); + + React.useEffect(() => { + if (fragmentRef === null) { + return; + } + + const dispose = fragmentRef.retain(); + const removeListener = fragmentRef.listen((promise) => { + setPromise([fragmentRef.key, promise]); + }); + + return () => { + dispose(); + removeListener(); + }; + }, [fragmentRef]); + + if (fragmentRef === null) { + return { data: null }; + } + + if (current[0] !== fragmentRef.key) { + // eslint-disable-next-line react-compiler/react-compiler + current[0] = fragmentRef.key; + current[1] = fragmentRef.promise; + } + + const data = __use(current[1]); + + return { data }; +} diff --git a/src/react/hooks/useSuspenseQuery.ts b/src/react/hooks/useSuspenseQuery.ts index faa0f44df6c..af634110fe6 100644 --- a/src/react/hooks/useSuspenseQuery.ts +++ b/src/react/hooks/useSuspenseQuery.ts @@ -11,6 +11,7 @@ import type { WatchQueryOptions, } from "../../core/index.js"; import { ApolloError, NetworkStatus } from "../../core/index.js"; +import type { SubscribeToMoreFunction } from "../../core/watchQueryOptions.js"; import type { DeepPartial } from "../../utilities/index.js"; import { isNonEmptyArray } from "../../utilities/index.js"; import { useApolloClient } from "./useApolloClient.js"; @@ -58,11 +59,6 @@ export type RefetchFunction< TVariables extends OperationVariables, > = ObservableQueryFields["refetch"]; -export type SubscribeToMoreFunction< - TData, - TVariables extends OperationVariables, -> = ObservableQueryFields["subscribeToMore"]; - export function useSuspenseQuery< TData, TVariables extends OperationVariables, @@ -277,7 +273,9 @@ function useSuspenseQuery_< [queryRef] ); - const subscribeToMore = queryRef.observable.subscribeToMore; + // TODO: The internalQueryRef doesn't have TVariables' type information so we have to cast it here + const subscribeToMore = queryRef.observable + .subscribeToMore as SubscribeToMoreFunction; return React.useMemo< UseSuspenseQueryResult diff --git a/src/react/internal/cache/FragmentReference.ts b/src/react/internal/cache/FragmentReference.ts new file mode 100644 index 00000000000..85453892bcc --- /dev/null +++ b/src/react/internal/cache/FragmentReference.ts @@ -0,0 +1,200 @@ +import { equal } from "@wry/equality"; +import type { + WatchFragmentOptions, + WatchFragmentResult, +} from "../../../cache/index.js"; +import type { ApolloClient } from "../../../core/ApolloClient.js"; +import type { MaybeMasked } from "../../../masking/index.js"; +import { + createFulfilledPromise, + wrapPromiseWithState, +} from "../../../utilities/index.js"; +import type { + Observable, + ObservableSubscription, + PromiseWithState, +} from "../../../utilities/index.js"; +import type { FragmentKey } from "./types.js"; + +type FragmentRefPromise = PromiseWithState; +type Listener = (promise: FragmentRefPromise) => void; + +interface FragmentReferenceOptions { + autoDisposeTimeoutMs?: number; + onDispose?: () => void; +} + +export class FragmentReference< + TData = unknown, + TVariables = Record, +> { + public readonly observable: Observable>; + public readonly key: FragmentKey = {}; + public promise!: FragmentRefPromise>; + + private resolve: ((result: MaybeMasked) => void) | undefined; + private reject: ((error: unknown) => void) | undefined; + + private subscription!: ObservableSubscription; + private listeners = new Set>>(); + private autoDisposeTimeoutId?: NodeJS.Timeout; + + private references = 0; + + constructor( + client: ApolloClient, + watchFragmentOptions: WatchFragmentOptions & { + from: string; + }, + options: FragmentReferenceOptions + ) { + this.dispose = this.dispose.bind(this); + this.handleNext = this.handleNext.bind(this); + this.handleError = this.handleError.bind(this); + + this.observable = client.watchFragment(watchFragmentOptions); + + if (options.onDispose) { + this.onDispose = options.onDispose; + } + + const diff = this.getDiff(client, watchFragmentOptions); + + // Start a timer that will automatically dispose of the query if the + // suspended resource does not use this fragmentRef in the given time. This + // helps prevent memory leaks when a component has unmounted before the + // query has finished loading. + const startDisposeTimer = () => { + if (!this.references) { + this.autoDisposeTimeoutId = setTimeout( + this.dispose, + options.autoDisposeTimeoutMs ?? 30_000 + ); + } + }; + + this.promise = + diff.complete ? + createFulfilledPromise(diff.result) + : this.createPendingPromise(); + this.subscribeToFragment(); + + this.promise.then(startDisposeTimer, startDisposeTimer); + } + + listen(listener: Listener>) { + this.listeners.add(listener); + + return () => { + this.listeners.delete(listener); + }; + } + + retain() { + this.references++; + clearTimeout(this.autoDisposeTimeoutId); + let disposed = false; + + return () => { + if (disposed) { + return; + } + + disposed = true; + this.references--; + + setTimeout(() => { + if (!this.references) { + this.dispose(); + } + }); + }; + } + + private dispose() { + this.subscription.unsubscribe(); + this.onDispose(); + } + + private onDispose() { + // noop. overridable by options + } + + private subscribeToFragment() { + this.subscription = this.observable.subscribe( + this.handleNext.bind(this), + this.handleError.bind(this) + ); + } + + private handleNext(result: WatchFragmentResult) { + switch (this.promise.status) { + case "pending": { + if (result.complete) { + return this.resolve?.(result.data); + } + + this.deliver(this.promise); + break; + } + case "fulfilled": { + // This can occur when we already have a result written to the cache and + // we subscribe for the first time. We create a fulfilled promise in the + // constructor with a value that is the same as the first emitted value + // so we want to skip delivering it. + if (equal(this.promise.value, result.data)) { + return; + } + + this.promise = + result.complete ? + createFulfilledPromise(result.data) + : this.createPendingPromise(); + + this.deliver(this.promise); + } + } + } + + private handleError(error: unknown) { + this.reject?.(error); + } + + private deliver(promise: FragmentRefPromise>) { + this.listeners.forEach((listener) => listener(promise)); + } + + private createPendingPromise() { + return wrapPromiseWithState( + new Promise>((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }) + ); + } + + private getDiff( + client: ApolloClient, + options: WatchFragmentOptions & { from: string } + ) { + const { cache } = client; + const { from, fragment, fragmentName } = options; + + const diff = cache.diff({ + ...options, + query: cache["getFragmentDoc"](fragment, fragmentName), + returnPartialData: true, + id: from, + optimistic: true, + }); + + return { + ...diff, + result: client["queryManager"].maskFragment({ + fragment, + fragmentName, + data: diff.result, + }) as MaybeMasked, + }; + } +} diff --git a/src/react/internal/cache/QueryReference.ts b/src/react/internal/cache/QueryReference.ts index 2e8ec0b276b..a719a83a232 100644 --- a/src/react/internal/cache/QueryReference.ts +++ b/src/react/internal/cache/QueryReference.ts @@ -29,8 +29,10 @@ type FetchMoreOptions = Parameters< ObservableQuery["fetchMore"] >[0]; -const QUERY_REFERENCE_SYMBOL: unique symbol = Symbol(); -const PROMISE_SYMBOL: unique symbol = Symbol(); +const QUERY_REFERENCE_SYMBOL: unique symbol = Symbol.for( + "apollo.internal.queryRef" +); +const PROMISE_SYMBOL: unique symbol = Symbol.for("apollo.internal.refPromise"); declare const QUERY_REF_BRAND: unique symbol; /** * A `QueryReference` is an opaque object returned by `useBackgroundQuery`. diff --git a/src/react/internal/cache/SuspenseCache.ts b/src/react/internal/cache/SuspenseCache.ts index 8b1eba321b5..03bf0c43049 100644 --- a/src/react/internal/cache/SuspenseCache.ts +++ b/src/react/internal/cache/SuspenseCache.ts @@ -1,8 +1,13 @@ import { Trie } from "@wry/trie"; -import type { ObservableQuery } from "../../../core/index.js"; +import type { + ApolloClient, + ObservableQuery, + WatchFragmentOptions, +} from "../../../core/index.js"; import { canUseWeakMap } from "../../../utilities/index.js"; import { InternalQueryReference } from "./QueryReference.js"; -import type { CacheKey } from "./types.js"; +import type { CacheKey, FragmentCacheKey } from "./types.js"; +import { FragmentReference } from "./FragmentReference.js"; export interface SuspenseCacheOptions { /** @@ -22,6 +27,10 @@ export class SuspenseCache { private queryRefs = new Trie<{ current?: InternalQueryReference }>( canUseWeakMap ); + private fragmentRefs = new Trie<{ current?: FragmentReference }>( + canUseWeakMap + ); + private options: SuspenseCacheOptions; constructor(options: SuspenseCacheOptions = Object.create(null)) { @@ -48,6 +57,27 @@ export class SuspenseCache { return ref.current; } + getFragmentRef( + cacheKey: FragmentCacheKey, + client: ApolloClient, + options: WatchFragmentOptions & { from: string } + ) { + const ref = this.fragmentRefs.lookupArray(cacheKey) as { + current?: FragmentReference; + }; + + if (!ref.current) { + ref.current = new FragmentReference(client, options, { + autoDisposeTimeoutMs: this.options.autoDisposeTimeoutMs, + onDispose: () => { + delete ref.current; + }, + }); + } + + return ref.current; + } + add(cacheKey: CacheKey, queryRef: InternalQueryReference) { const ref = this.queryRefs.lookupArray(cacheKey); ref.current = queryRef; diff --git a/src/react/internal/cache/types.ts b/src/react/internal/cache/types.ts index 40f3c4cc8fc..a163431ad9d 100644 --- a/src/react/internal/cache/types.ts +++ b/src/react/internal/cache/types.ts @@ -6,6 +6,16 @@ export type CacheKey = [ ...queryKey: any[], ]; +export type FragmentCacheKey = [ + cacheId: string, + fragment: DocumentNode, + stringifiedVariables: string, +]; + export interface QueryKey { __queryKey?: string; } + +export interface FragmentKey { + __fragmentKey?: string; +} diff --git a/src/react/query-preloader/createQueryPreloader.ts b/src/react/query-preloader/createQueryPreloader.ts index 6389992519c..50b9e500579 100644 --- a/src/react/query-preloader/createQueryPreloader.ts +++ b/src/react/query-preloader/createQueryPreloader.ts @@ -15,25 +15,9 @@ import type { } from "../../utilities/index.js"; import { InternalQueryReference, wrapQueryRef } from "../internal/index.js"; import type { PreloadedQueryRef } from "../internal/index.js"; -import type { NoInfer } from "../index.js"; +import type { NoInfer, VariablesOption } from "../index.js"; import { wrapHook } from "../hooks/internal/index.js"; -type VariablesOption = - [TVariables] extends [never] ? - { - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ - variables?: Record; - } - : {} extends OnlyRequiredProperties ? - { - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ - variables?: TVariables; - } - : { - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ - variables: TVariables; - }; - export type PreloadQueryFetchPolicy = Extract< WatchQueryFetchPolicy, "cache-first" | "network-only" | "no-cache" | "cache-and-network" diff --git a/src/react/types/types.ts b/src/react/types/types.ts index 7812fb34bc2..5e82f4dc8fc 100644 --- a/src/react/types/types.ts +++ b/src/react/types/types.ts @@ -5,6 +5,7 @@ import type { TypedDocumentNode } from "@graphql-typed-document-node/core"; import type { Observable, ObservableSubscription, + OnlyRequiredProperties, } from "../../utilities/index.js"; import type { FetchResult } from "../../link/core/index.js"; import type { ApolloError } from "../../errors/index.js"; @@ -19,7 +20,6 @@ import type { InternalRefetchQueriesInclude, WatchQueryOptions, WatchQueryFetchPolicy, - SubscribeToMoreOptions, ApolloQueryResult, FetchMoreQueryOptions, ErrorPolicy, @@ -28,6 +28,8 @@ import type { import type { MutationSharedOptions, SharedWatchQueryOptions, + SubscribeToMoreFunction, + UpdateQueryMapFn, } from "../../core/watchQueryOptions.js"; import type { MaybeMasked, Unmasked } from "../../masking/index.js"; @@ -67,9 +69,19 @@ export interface QueryFunctionOptions< > extends BaseQueryOptions { /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#skip:member} */ skip?: boolean; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} */ + /** + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} + * + * @deprecated This option will be removed in the next major version of Apollo Client. + * For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + */ onCompleted?: (data: MaybeMasked) => void; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} */ + /** + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} + * + * @deprecated This option will be removed in the next major version of Apollo Client. + * For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + */ onError?: (error: ApolloError) => void; // Default WatchQueryOptions for this useQuery, providing initial values for @@ -90,23 +102,9 @@ export interface ObservableQueryFields< /** {@inheritDoc @apollo/client!QueryResultDocumentation#stopPolling:member} */ stopPolling: () => void; /** {@inheritDoc @apollo/client!QueryResultDocumentation#subscribeToMore:member} */ - subscribeToMore: < - TSubscriptionData = TData, - TSubscriptionVariables extends OperationVariables = TVariables, - >( - options: SubscribeToMoreOptions< - TData, - TSubscriptionVariables, - TSubscriptionData - > - ) => () => void; + subscribeToMore: SubscribeToMoreFunction; /** {@inheritDoc @apollo/client!QueryResultDocumentation#updateQuery:member} */ - updateQuery: ( - mapFn: ( - previousQueryResult: Unmasked, - options: Pick, "variables"> - ) => Unmasked - ) => void; + updateQuery: (mapFn: UpdateQueryMapFn) => void; /** {@inheritDoc @apollo/client!QueryResultDocumentation#refetch:member} */ refetch: ( variables?: Partial @@ -180,9 +178,19 @@ export interface LazyQueryHookOptions< TData = any, TVariables extends OperationVariables = OperationVariables, > extends BaseQueryOptions { - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} */ + /** + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#onCompleted:member} + * + * @deprecated This option will be removed in the next major version of Apollo Client. + * For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + */ onCompleted?: (data: MaybeMasked) => void; - /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} */ + /** + * {@inheritDoc @apollo/client!QueryOptionsDocumentation#onError:member} + * + * @deprecated This option will be removed in the next major version of Apollo Client. + * For more context, please see the [related issue](https://github.com/apollographql/apollo-client/issues/12352) on GitHub. + */ onError?: (error: ApolloError) => void; /** @internal */ @@ -358,7 +366,12 @@ export interface BaseMutationOptions< ) => void; /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#onError:member} */ onError?: (error: ApolloError, clientOptions?: BaseMutationOptions) => void; - /** {@inheritDoc @apollo/client!MutationOptionsDocumentation#ignoreResults:member} */ + /** + * {@inheritDoc @apollo/client!MutationOptionsDocumentation#ignoreResults:member} + * + * @deprecated This option will be removed in the next major version of Apollo Client. + * If you don't want to synchronize your component state with the mutation, please use `useApolloClient` to get your ApolloClient instance and call `client.mutate` directly. + */ ignoreResults?: boolean; } @@ -515,4 +528,20 @@ export interface SubscriptionCurrentObservable { subscription?: ObservableSubscription; } +export type VariablesOption = + [TVariables] extends [never] ? + { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables?: Record; + } + : Record extends OnlyRequiredProperties ? + { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables?: TVariables; + } + : { + /** {@inheritDoc @apollo/client!QueryOptionsDocumentation#variables:member} */ + variables: TVariables; + }; + export type { NoInfer } from "../../utilities/index.js"; diff --git a/src/testing/core/mocking/__tests__/__snapshots__/mockLink.ts.snap b/src/testing/core/mocking/__tests__/__snapshots__/mockLink.ts.snap new file mode 100644 index 00000000000..c25d45c175e --- /dev/null +++ b/src/testing/core/mocking/__tests__/__snapshots__/mockLink.ts.snap @@ -0,0 +1,14 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`shows undefined and NaN in debug messages 1`] = ` +"No more mocked responses for the query: query ($id: ID!, $filter: Boolean) { + usersByTestId(id: $id, filter: $filter) { + id + } +} +Expected variables: {\\"id\\":NaN,\\"filter\\":} + +Failed to match 1 mock for this query. The mocked response had the following variables: + {\\"id\\":1,\\"filter\\":true} +" +`; diff --git a/src/testing/core/mocking/__tests__/mockLink.ts b/src/testing/core/mocking/__tests__/mockLink.ts index fe618ae4f48..961c745ce70 100644 --- a/src/testing/core/mocking/__tests__/mockLink.ts +++ b/src/testing/core/mocking/__tests__/mockLink.ts @@ -1,7 +1,11 @@ import gql from "graphql-tag"; import { MockLink, MockedResponse } from "../mockLink"; import { execute } from "../../../../link/core/execute"; -import { ObservableStream, enableFakeTimers } from "../../../internal"; +import { + ObservableStream, + enableFakeTimers, + spyOnConsole, +} from "../../../internal"; describe("MockedResponse.newData", () => { const setup = () => { @@ -357,6 +361,34 @@ test("removes fields with @client directives", async () => { } }); +test("shows undefined and NaN in debug messages", async () => { + using _ = spyOnConsole("warn"); + + const query = gql` + query ($id: ID!, $filter: Boolean) { + usersByTestId(id: $id, filter: $filter) { + id + } + } + `; + + const link = new MockLink([ + { + request: { query, variables: { id: 1, filter: true } }, + // The actual response data makes no difference in this test + result: { data: { usersByTestId: null } }, + }, + ]); + + const stream = new ObservableStream( + execute(link, { query, variables: { id: NaN, filter: undefined } }) + ); + + const error = await stream.takeError(); + + expect(error.message).toMatchSnapshot(); +}); + describe.skip("type tests", () => { const ANY = {} as any; test("covariant behaviour: `MockedResponses` should be assignable to `MockedResponse`", () => { diff --git a/src/testing/core/mocking/mockLink.ts b/src/testing/core/mocking/mockLink.ts index 898efe6bfd3..dfae6ed60b0 100644 --- a/src/testing/core/mocking/mockLink.ts +++ b/src/testing/core/mocking/mockLink.ts @@ -14,12 +14,12 @@ import { addTypenameToDocument, removeClientSetsFromDocument, cloneDeep, - stringifyForDisplay, print, getOperationDefinition, getDefaultValues, removeDirectivesFromDocument, checkDocument, + makeUniqueId, } from "../../../utilities/index.js"; import type { Unmasked } from "../../../masking/index.js"; @@ -136,14 +136,14 @@ export class MockLink extends ApolloLink { if (!response) { configError = new Error( `No more mocked responses for the query: ${print(operation.query)} -Expected variables: ${stringifyForDisplay(operation.variables)} +Expected variables: ${stringifyForDebugging(operation.variables)} ${ unmatchedVars.length > 0 ? ` Failed to match ${unmatchedVars.length} mock${ unmatchedVars.length === 1 ? "" : "s" } for this query. The mocked response had the following variables: -${unmatchedVars.map((d) => ` ${stringifyForDisplay(d)}`).join("\n")} +${unmatchedVars.map((d) => ` ${stringifyForDebugging(d)}`).join("\n")} ` : "" }` @@ -280,3 +280,30 @@ export function mockSingleLink(...mockedResponses: Array): MockApolloLink { return new MockLink(mocks, maybeTypename); } + +// This is similiar to the stringifyForDisplay utility we ship, but includes +// support for NaN in addition to undefined. More values may be handled in the +// future. This is not added to the primary stringifyForDisplay helper since it +// is used for the cache and other purposes. We need this for debuggging only. +export function stringifyForDebugging(value: any, space = 0): string { + const undefId = makeUniqueId("undefined"); + const nanId = makeUniqueId("NaN"); + + return JSON.stringify( + value, + (_, value) => { + if (value === void 0) { + return undefId; + } + + if (Number.isNaN(value)) { + return nanId; + } + + return value; + }, + space + ) + .replace(new RegExp(JSON.stringify(undefId), "g"), "") + .replace(new RegExp(JSON.stringify(nanId), "g"), "NaN"); +} diff --git a/src/utilities/subscriptions/urql/index.ts b/src/utilities/subscriptions/urql/index.ts index 26bf4d4fb57..a8f195562ec 100644 --- a/src/utilities/subscriptions/urql/index.ts +++ b/src/utilities/subscriptions/urql/index.ts @@ -35,7 +35,9 @@ export function createFetchMultipartSubscription( const currentFetch = preferredFetch || maybe(() => fetch) || backupFetch; const observerNext = observer.next.bind(observer); - currentFetch!(uri, options) + const abortController = new AbortController(); + + currentFetch!(uri, { ...options, signal: abortController.signal }) .then((response) => { const ctype = response.headers?.get("content-type"); @@ -51,6 +53,10 @@ export function createFetchMultipartSubscription( .catch((err: any) => { handleError(err, observer); }); + + return () => { + abortController.abort(); + }; }); }; } diff --git a/src/utilities/types/DeepOmit.ts b/src/utilities/types/DeepOmit.ts index 9d1e31e6817..6a850dbc06c 100644 --- a/src/utilities/types/DeepOmit.ts +++ b/src/utilities/types/DeepOmit.ts @@ -22,7 +22,7 @@ export type DeepOmitArray = { export type DeepOmit = T extends DeepOmitPrimitive ? T : { - [P in Exclude]: T[P] extends infer TP ? + [P in keyof T as P extends K ? never : P]: T[P] extends infer TP ? TP extends DeepOmitPrimitive ? TP : TP extends any[] ? DeepOmitArray : DeepOmit