UNPKG

@apollo/client

Version:

A fully-featured caching GraphQL client.

1 lines 223 kB
{"version":3,"file":"QueryManager.cjs","sources":["../../../src/core/QueryManager.ts"],"sourcesContent":["import { Trie } from \"@wry/trie\";\nimport type {\n DirectiveNode,\n DocumentNode,\n FormattedExecutionResult,\n} from \"graphql\";\nimport { BREAK, Kind, OperationTypeNode, visit } from \"graphql\";\nimport { Observable, throwError } from \"rxjs\";\nimport {\n catchError,\n concat,\n EMPTY,\n filter,\n finalize,\n from,\n lastValueFrom,\n map,\n materialize,\n mergeMap,\n of,\n share,\n shareReplay,\n Subject,\n tap,\n} from \"rxjs\";\n\nimport type { ApolloCache, Cache } from \"@apollo/client/cache\";\nimport { canonicalStringify } from \"@apollo/client/cache\";\nimport {\n CombinedGraphQLErrors,\n graphQLResultHasProtocolErrors,\n registerLinkError,\n toErrorLike,\n} from \"@apollo/client/errors\";\nimport { PROTOCOL_ERRORS_SYMBOL } from \"@apollo/client/errors\";\nimport type { Incremental } from \"@apollo/client/incremental\";\nimport type { ApolloLink } from \"@apollo/client/link\";\nimport { execute } from \"@apollo/client/link\";\nimport type { LocalState } from \"@apollo/client/local-state\";\nimport type { MaybeMasked } from \"@apollo/client/masking\";\nimport { maskFragment, maskOperation } from \"@apollo/client/masking\";\nimport type { DeepPartial } from \"@apollo/client/utilities\";\nimport {\n cacheSizes,\n DocumentTransform,\n isNetworkRequestInFlight,\n print,\n} from \"@apollo/client/utilities\";\nimport { __DEV__ } from \"@apollo/client/utilities/environment\";\nimport {\n AutoCleanedWeakCache,\n checkDocument,\n filterMap,\n getDefaultValues,\n getOperationDefinition,\n getOperationName,\n graphQLResultHasError,\n hasDirectives,\n hasForcedResolvers,\n isDocumentNode,\n isNonNullObject,\n makeUniqueId,\n removeDirectivesFromDocument,\n toQueryResult,\n} from \"@apollo/client/utilities/internal\";\nimport {\n invariant,\n newInvariantError,\n} from \"@apollo/client/utilities/invariant\";\n\nimport { defaultCacheSizes } from \"../utilities/caching/sizes.js\";\n\nimport type { ApolloClient } from \"./ApolloClient.js\";\nimport { NetworkStatus } from \"./networkStatus.js\";\nimport { logMissingFieldErrors, ObservableQuery } from \"./ObservableQuery.js\";\nimport { CacheWriteBehavior, QueryInfo } from \"./QueryInfo.js\";\nimport type {\n DefaultContext,\n InternalRefetchQueriesInclude,\n InternalRefetchQueriesMap,\n InternalRefetchQueriesOptions,\n InternalRefetchQueriesResult,\n OperationVariables,\n QueryNotification,\n SubscriptionObservable,\n TypedDocumentNode,\n} from \"./types.js\";\nimport type {\n ErrorPolicy,\n MutationFetchPolicy,\n WatchQueryFetchPolicy,\n} from \"./watchQueryOptions.js\";\n\ninterface MutationStoreValue {\n mutation: DocumentNode;\n variables: Record<string, any>;\n loading: boolean;\n error: Error | null;\n}\n\ninterface TransformCacheEntry {\n hasClientExports: boolean;\n hasForcedResolvers: boolean;\n hasNonreactiveDirective: boolean;\n hasIncrementalDirective: boolean;\n nonReactiveQuery: DocumentNode;\n clientQuery: DocumentNode | null;\n serverQuery: DocumentNode | null;\n defaultVars: OperationVariables;\n asQuery: DocumentNode;\n operationType: OperationTypeNode | undefined;\n violation?: Error | undefined;\n}\n\ninterface MaskFragmentOptions<TData> {\n fragment: DocumentNode;\n data: TData;\n fragmentName?: string;\n}\n\ninterface MaskOperationOptions<TData> {\n document: DocumentNode;\n data: TData;\n /**\n * Can be used to identify the cause to prevent warning for the same cause twice.\n * This would be an object like e.g. an `ObervableQuery`.\n * If the `cause` is not provided, we will warn every time.\n */\n cause?: object;\n fetchPolicy?: WatchQueryFetchPolicy;\n}\n\ninterface QueryManagerOptions {\n client: ApolloClient;\n clientOptions: ApolloClient.Options;\n defaultOptions: ApolloClient.DefaultOptions;\n documentTransform: DocumentTransform | null | undefined;\n queryDeduplication: boolean;\n onBroadcast: undefined | (() => void);\n ssrMode: boolean;\n assumeImmutableResults: boolean;\n defaultContext: Partial<DefaultContext> | undefined;\n dataMasking: boolean;\n localState: LocalState | undefined;\n incrementalHandler: Incremental.Handler;\n}\n\nexport class QueryManager {\n public defaultOptions: ApolloClient.DefaultOptions;\n\n public readonly client: ApolloClient;\n /**\n * The options that were passed to the ApolloClient constructor.\n */\n public readonly clientOptions: ApolloClient.Options;\n public readonly assumeImmutableResults: boolean;\n public readonly documentTransform: DocumentTransform;\n public readonly ssrMode: boolean;\n public readonly defaultContext: Partial<DefaultContext>;\n public readonly dataMasking: boolean;\n public readonly incrementalHandler: Incremental.Handler;\n public localState: LocalState | undefined;\n\n private queryDeduplication: boolean;\n\n /**\n * Whether to prioritize cache values over network results when\n * `fetchObservableWithInfo` is called.\n * This will essentially turn a `\"network-only\"` or `\"cache-and-network\"`\n * fetchPolicy into a `\"cache-first\"` fetchPolicy, but without influencing\n * the `fetchPolicy` of the `ObservableQuery`.\n *\n * This can e.g. be used to prioritize the cache during the first render after\n * SSR.\n */\n public prioritizeCacheValues: boolean = false;\n\n private onBroadcast?: () => void;\n public mutationStore?: {\n [mutationId: string]: MutationStoreValue;\n };\n\n /**\n * All ObservableQueries that currently have at least one subscriber.\n */\n public obsQueries = new Set<ObservableQuery<any, any>>();\n\n // Maps from queryInfo.id strings to Promise rejection functions for\n // currently active queries and fetches.\n // Use protected instead of private field so\n // @apollo/experimental-nextjs-app-support can access type info.\n protected fetchCancelFns = new Map<string, (error: any) => any>();\n\n constructor(options: QueryManagerOptions) {\n const defaultDocumentTransform = new DocumentTransform(\n (document) => this.cache.transformDocument(document),\n // Allow the apollo cache to manage its own transform caches\n { cache: false }\n );\n\n this.client = options.client;\n this.defaultOptions = options.defaultOptions;\n this.queryDeduplication = options.queryDeduplication;\n this.clientOptions = options.clientOptions;\n this.ssrMode = options.ssrMode;\n this.assumeImmutableResults = options.assumeImmutableResults;\n this.dataMasking = options.dataMasking;\n this.localState = options.localState;\n this.incrementalHandler = options.incrementalHandler;\n const documentTransform = options.documentTransform;\n this.documentTransform =\n documentTransform ?\n defaultDocumentTransform\n .concat(documentTransform)\n // The custom document transform may add new fragment spreads or new\n // field selections, so we want to give the cache a chance to run\n // again. For example, the InMemoryCache adds __typename to field\n // selections and fragments from the fragment registry.\n .concat(defaultDocumentTransform)\n : defaultDocumentTransform;\n this.defaultContext = options.defaultContext || {};\n\n if ((this.onBroadcast = options.onBroadcast)) {\n this.mutationStore = {};\n }\n }\n\n get link() {\n return this.client.link;\n }\n\n get cache() {\n return this.client.cache;\n }\n\n /**\n * Call this method to terminate any active query processes, making it safe\n * to dispose of this QueryManager instance.\n */\n public stop() {\n this.obsQueries.forEach((oq) => oq.stop());\n\n this.cancelPendingFetches(\n newInvariantError(\"QueryManager stopped while query was in flight\")\n );\n }\n\n private cancelPendingFetches(error: Error) {\n this.fetchCancelFns.forEach((cancel) => cancel(error));\n this.fetchCancelFns.clear();\n }\n\n public async mutate<\n TData,\n TVariables extends OperationVariables,\n TCache extends ApolloCache,\n >({\n mutation,\n variables,\n optimisticResponse,\n updateQueries,\n refetchQueries = [],\n awaitRefetchQueries = false,\n update: updateWithProxyFn,\n onQueryUpdated,\n fetchPolicy,\n errorPolicy,\n keepRootFields,\n context,\n }: ApolloClient.MutateOptions<TData, TVariables, TCache> & {\n errorPolicy: ErrorPolicy;\n fetchPolicy: MutationFetchPolicy;\n }): Promise<ApolloClient.MutateResult<MaybeMasked<TData>>> {\n const queryInfo = new QueryInfo<TData, TVariables, TCache>(this);\n\n mutation = this.cache.transformForLink(this.transform(mutation));\n const { hasClientExports } = this.getDocumentInfo(mutation);\n\n variables = this.getVariables(mutation, variables);\n\n if (hasClientExports) {\n if (__DEV__) {\n invariant(\n this.localState,\n \"Mutation '%s' contains `@client` fields with variables provided by `@export` but local state has not been configured.\",\n getOperationName(mutation, \"(anonymous)\")\n );\n }\n\n variables = await this.localState!.getExportedVariables<TVariables>({\n client: this.client,\n document: mutation,\n variables,\n context,\n });\n }\n\n const mutationStoreValue =\n this.mutationStore &&\n (this.mutationStore[queryInfo.id] = {\n mutation,\n variables,\n loading: true,\n error: null,\n } as MutationStoreValue);\n\n const isOptimistic =\n optimisticResponse &&\n queryInfo.markMutationOptimistic(optimisticResponse, {\n document: mutation,\n variables,\n cacheWriteBehavior:\n fetchPolicy === \"no-cache\" ?\n CacheWriteBehavior.FORBID\n : CacheWriteBehavior.MERGE,\n errorPolicy,\n context,\n updateQueries,\n update: updateWithProxyFn,\n keepRootFields,\n });\n\n this.broadcastQueries();\n\n return new Promise((resolve, reject) => {\n const cause = {};\n return this.getObservableFromLink<TData>(\n mutation,\n {\n ...context,\n optimisticResponse: isOptimistic ? optimisticResponse : void 0,\n },\n variables,\n {},\n false\n )\n .observable.pipe(\n validateDidEmitValue(),\n mergeMap((result) => {\n const storeResult: typeof result = { ...result };\n\n return from(\n queryInfo.markMutationResult(storeResult, {\n document: mutation,\n variables,\n cacheWriteBehavior:\n fetchPolicy === \"no-cache\" ?\n CacheWriteBehavior.FORBID\n : CacheWriteBehavior.MERGE,\n errorPolicy,\n context,\n update: updateWithProxyFn,\n updateQueries,\n awaitRefetchQueries,\n refetchQueries,\n removeOptimistic: isOptimistic ? queryInfo.id : void 0,\n onQueryUpdated,\n keepRootFields,\n })\n );\n })\n )\n .pipe(\n map((storeResult) => {\n const hasErrors = graphQLResultHasError(storeResult);\n if (hasErrors && errorPolicy === \"none\") {\n throw new CombinedGraphQLErrors(storeResult);\n }\n\n if (mutationStoreValue) {\n mutationStoreValue.loading = false;\n mutationStoreValue.error = null;\n }\n\n return storeResult;\n })\n )\n .subscribe({\n next: (storeResult) => {\n this.broadcastQueries();\n\n // Since mutations might receive multiple payloads from the\n // ApolloLink chain (e.g. when used with @defer),\n // we resolve with a SingleExecutionResult or after the final\n // ExecutionPatchResult has arrived and we have assembled the\n // multipart response into a single result.\n if (!queryInfo.hasNext) {\n const result: ApolloClient.MutateResult<TData> = {\n data: this.maskOperation({\n document: mutation,\n data: storeResult.data,\n fetchPolicy,\n cause,\n }) as any,\n };\n\n if (graphQLResultHasError(storeResult)) {\n result.error = new CombinedGraphQLErrors(storeResult);\n }\n\n if (Object.keys(storeResult.extensions || {}).length) {\n result.extensions = storeResult.extensions;\n }\n\n resolve(result);\n }\n },\n\n error: (error) => {\n if (mutationStoreValue) {\n mutationStoreValue.loading = false;\n mutationStoreValue.error = error;\n }\n\n if (isOptimistic) {\n this.cache.removeOptimistic(queryInfo.id);\n }\n\n this.broadcastQueries();\n\n if (errorPolicy === \"ignore\") {\n return resolve({ data: undefined });\n }\n\n if (errorPolicy === \"all\") {\n return resolve({ data: undefined, error });\n }\n\n reject(error);\n },\n });\n });\n }\n\n public fetchQuery<TData, TVariables extends OperationVariables>(\n options: ApolloClient.WatchQueryOptions<TData, TVariables>,\n networkStatus?: NetworkStatus\n ): Promise<ApolloClient.QueryResult<TData>> {\n checkDocument(options.query, OperationTypeNode.QUERY);\n\n // do the rest asynchronously to keep the same rejection timing as\n // checks further in `.mutate`\n return (async () =>\n lastValueFrom(\n this.fetchObservableWithInfo(options, {\n networkStatus,\n }).observable.pipe(\n filterMap((value) => {\n switch (value.kind) {\n case \"E\":\n throw value.error;\n case \"N\": {\n if (value.source !== \"newNetworkStatus\")\n return toQueryResult(value.value);\n }\n }\n })\n ),\n {\n // This default is needed when a `standby` fetch policy is used to avoid\n // an EmptyError from rejecting this promise.\n defaultValue: { data: undefined },\n }\n ))();\n }\n\n public transform(document: DocumentNode) {\n return this.documentTransform.transformDocument(document);\n }\n\n private transformCache = new AutoCleanedWeakCache<\n DocumentNode,\n TransformCacheEntry\n >(\n cacheSizes[\"queryManager.getDocumentInfo\"] ||\n defaultCacheSizes[\"queryManager.getDocumentInfo\"]\n );\n\n public getDocumentInfo(document: DocumentNode) {\n const { transformCache } = this;\n\n if (!transformCache.has(document)) {\n const operationDefinition = getOperationDefinition(document);\n\n const cacheEntry: TransformCacheEntry = {\n // TODO These three calls (hasClientExports, shouldForceResolvers, and\n // usesNonreactiveDirective) are performing independent full traversals\n // of the transformed document. We should consider merging these\n // traversals into a single pass in the future, though the work is\n // cached after the first time.\n hasClientExports: hasDirectives([\"client\", \"export\"], document, true),\n hasForcedResolvers: hasForcedResolvers(document),\n hasNonreactiveDirective: hasDirectives([\"nonreactive\"], document),\n hasIncrementalDirective: hasDirectives([\"defer\"], document),\n nonReactiveQuery: addNonReactiveToNamedFragments(document),\n clientQuery: hasDirectives([\"client\"], document) ? document : null,\n serverQuery: removeDirectivesFromDocument(\n [\n { name: \"client\", remove: true },\n { name: \"connection\" },\n { name: \"nonreactive\" },\n { name: \"unmask\" },\n ],\n document\n ),\n operationType: operationDefinition?.operation,\n defaultVars: getDefaultValues(\n operationDefinition\n ) as OperationVariables,\n // Transform any mutation or subscription operations to query operations\n // so we can read/write them from/to the cache.\n asQuery: {\n ...document,\n definitions: document.definitions.map((def) => {\n if (\n def.kind === \"OperationDefinition\" &&\n def.operation !== \"query\"\n ) {\n return { ...def, operation: \"query\" as OperationTypeNode };\n }\n return def;\n }),\n },\n };\n\n transformCache.set(document, cacheEntry);\n }\n\n const entry = transformCache.get(document)!;\n if (entry.violation) {\n throw entry.violation;\n }\n return entry;\n }\n\n public getVariables<TVariables extends OperationVariables>(\n document: DocumentNode,\n variables?: TVariables\n ): TVariables {\n const defaultVars = this.getDocumentInfo(document).defaultVars;\n const varsWithDefaults = Object.entries(variables ?? {}).map(\n ([key, value]) => [key, value === undefined ? defaultVars[key] : value]\n );\n\n return {\n ...defaultVars,\n ...Object.fromEntries(varsWithDefaults),\n };\n }\n\n public watchQuery<\n TData,\n TVariables extends OperationVariables = OperationVariables,\n >(\n options: ApolloClient.WatchQueryOptions<TData, TVariables>\n ): ObservableQuery<TData, TVariables> {\n checkDocument(options.query, OperationTypeNode.QUERY);\n\n const query = this.transform(options.query);\n\n // assign variable default values if supplied\n // NOTE: We don't modify options.query here with the transformed query to\n // ensure observable.options.query is set to the raw untransformed query.\n options = {\n ...options,\n variables: this.getVariables(query, options.variables) as TVariables,\n };\n\n if (typeof options.notifyOnNetworkStatusChange === \"undefined\") {\n options.notifyOnNetworkStatusChange = true;\n }\n\n const observable = new ObservableQuery<TData, TVariables>({\n queryManager: this,\n options,\n transformedQuery: query,\n });\n\n return observable;\n }\n\n public query<\n TData,\n TVariables extends OperationVariables = OperationVariables,\n >(\n options: ApolloClient.QueryOptions<TData, TVariables>\n ): Promise<ApolloClient.QueryResult<MaybeMasked<TData>>> {\n const query = this.transform(options.query);\n\n return this.fetchQuery<TData, TVariables>({\n ...(options as any),\n query,\n }).then((value) => ({\n ...value,\n data: this.maskOperation({\n document: query,\n data: value?.data,\n fetchPolicy: options.fetchPolicy,\n }),\n }));\n }\n\n private requestIdCounter = 1;\n public generateRequestId() {\n return this.requestIdCounter++;\n }\n\n public clearStore(\n options: Cache.ResetOptions = {\n discardWatches: true,\n }\n ): Promise<void> {\n // Before we have sent the reset action to the store, we can no longer\n // rely on the results returned by in-flight requests since these may\n // depend on values that previously existed in the data portion of the\n // store. So, we cancel the promises and observers that we have issued\n // so far and not yet resolved (in the case of queries).\n this.cancelPendingFetches(\n newInvariantError(\n \"Store reset while query was in flight (not completed in link chain)\"\n )\n );\n\n this.obsQueries.forEach((observableQuery) => {\n // Set loading to true so listeners don't trigger unless they want\n // results with partial data.\n observableQuery.reset();\n });\n\n if (this.mutationStore) {\n this.mutationStore = {};\n }\n\n // begin removing data from the store\n return this.cache.reset(options);\n }\n\n public getObservableQueries(\n include: InternalRefetchQueriesInclude = \"active\"\n ) {\n const queries = new Set<ObservableQuery<any>>();\n const queryNames = new Map<string, string | undefined>();\n const queryNamesAndQueryStrings = new Map<string, boolean>();\n const legacyQueryOptions = new Set<ApolloClient.QueryOptions>();\n\n if (Array.isArray(include)) {\n include.forEach((desc) => {\n if (typeof desc === \"string\") {\n queryNames.set(desc, desc);\n queryNamesAndQueryStrings.set(desc, false);\n } else if (isDocumentNode(desc)) {\n const queryString = print(this.transform(desc));\n queryNames.set(queryString, getOperationName(desc));\n queryNamesAndQueryStrings.set(queryString, false);\n } else if (isNonNullObject(desc) && desc.query) {\n legacyQueryOptions.add(desc);\n }\n });\n }\n\n this.obsQueries.forEach((oq) => {\n const document = print(this.transform(oq.options.query));\n if (include === \"all\") {\n queries.add(oq);\n return;\n }\n\n const {\n queryName,\n options: { fetchPolicy },\n } = oq;\n\n if (include === \"active\" && fetchPolicy === \"standby\") {\n return;\n }\n\n if (\n include === \"active\" ||\n (queryName && queryNamesAndQueryStrings.has(queryName)) ||\n (document && queryNamesAndQueryStrings.has(document))\n ) {\n queries.add(oq);\n if (queryName) queryNamesAndQueryStrings.set(queryName, true);\n if (document) queryNamesAndQueryStrings.set(document, true);\n }\n });\n\n if (legacyQueryOptions.size) {\n legacyQueryOptions.forEach((options) => {\n const oq = new ObservableQuery({\n queryManager: this,\n options: {\n ...options,\n fetchPolicy: \"network-only\",\n },\n });\n queries.add(oq);\n });\n }\n\n if (__DEV__ && queryNamesAndQueryStrings.size) {\n queryNamesAndQueryStrings.forEach((included, nameOrQueryString) => {\n if (!included) {\n const queryName = queryNames.get(nameOrQueryString);\n\n if (queryName) {\n invariant.warn(\n `Unknown query named \"%s\" requested in refetchQueries options.include array`,\n queryName\n );\n } else {\n invariant.warn(\n `Unknown anonymous query requested in refetchQueries options.include array`\n );\n }\n }\n });\n }\n\n return queries;\n }\n\n public refetchObservableQueries(\n includeStandby: boolean = false\n ): Promise<ApolloClient.QueryResult<any>[]> {\n const observableQueryPromises: Promise<ApolloClient.QueryResult<any>>[] =\n [];\n\n this.getObservableQueries(includeStandby ? \"all\" : \"active\").forEach(\n (observableQuery) => {\n const { fetchPolicy } = observableQuery.options;\n if (\n (includeStandby || fetchPolicy !== \"standby\") &&\n fetchPolicy !== \"cache-only\"\n ) {\n observableQueryPromises.push(observableQuery.refetch());\n }\n }\n );\n\n this.broadcastQueries();\n\n return Promise.all(observableQueryPromises);\n }\n\n public startGraphQLSubscription<TData = unknown>(\n options: ApolloClient.SubscribeOptions<TData>\n ): SubscriptionObservable<ApolloClient.SubscribeResult<TData>> {\n let { query, variables } = options;\n const {\n fetchPolicy,\n errorPolicy = \"none\",\n context = {},\n extensions = {},\n } = options;\n\n checkDocument(query, OperationTypeNode.SUBSCRIPTION);\n\n query = this.transform(query);\n variables = this.getVariables(query, variables);\n\n let restart: (() => void) | undefined;\n\n if (__DEV__) {\n invariant(\n !this.getDocumentInfo(query).hasClientExports || this.localState,\n \"Subscription '%s' contains `@client` fields with variables provided by `@export` but local state has not been configured.\",\n getOperationName(query, \"(anonymous)\")\n );\n }\n\n const observable = (\n this.getDocumentInfo(query).hasClientExports ?\n from(\n this.localState!.getExportedVariables({\n client: this.client,\n document: query,\n variables,\n context,\n })\n )\n : of(variables)).pipe(\n mergeMap((variables) => {\n const { observable, restart: res } = this.getObservableFromLink<TData>(\n query,\n context,\n variables,\n extensions\n );\n\n const queryInfo = new QueryInfo<TData>(this);\n\n restart = res;\n return (observable as Observable<FormattedExecutionResult<TData>>).pipe(\n map((rawResult): ApolloClient.SubscribeResult<TData> => {\n queryInfo.markSubscriptionResult(rawResult, {\n document: query,\n variables,\n errorPolicy,\n cacheWriteBehavior:\n fetchPolicy === \"no-cache\" ?\n CacheWriteBehavior.FORBID\n : CacheWriteBehavior.MERGE,\n });\n\n const result: ApolloClient.SubscribeResult<TData> = {\n data: rawResult.data ?? undefined,\n };\n\n if (graphQLResultHasError(rawResult)) {\n result.error = new CombinedGraphQLErrors(rawResult);\n } else if (graphQLResultHasProtocolErrors(rawResult)) {\n result.error = rawResult.extensions[PROTOCOL_ERRORS_SYMBOL];\n // Don't emit protocol errors added by HttpLink\n delete rawResult.extensions[PROTOCOL_ERRORS_SYMBOL];\n }\n\n if (\n rawResult.extensions &&\n Object.keys(rawResult.extensions).length\n ) {\n result.extensions = rawResult.extensions;\n }\n\n if (result.error && errorPolicy === \"none\") {\n result.data = undefined;\n }\n\n if (errorPolicy === \"ignore\") {\n delete result.error;\n }\n\n return result;\n }),\n catchError((error) => {\n if (errorPolicy === \"ignore\") {\n return of({\n data: undefined,\n } as ApolloClient.SubscribeResult<TData>);\n }\n\n return of({ data: undefined, error });\n }),\n filter((result) => !!(result.data || result.error))\n );\n })\n );\n\n return Object.assign(observable, { restart: () => restart?.() });\n }\n\n public broadcastQueries() {\n if (this.onBroadcast) this.onBroadcast();\n this.obsQueries.forEach((observableQuery) => observableQuery.notify());\n }\n\n // Use protected instead of private field so\n // @apollo/experimental-nextjs-app-support can access type info.\n protected inFlightLinkObservables = new Trie<{\n observable?: Observable<ApolloLink.Result<any>>;\n restart?: () => void;\n }>(false);\n\n private getObservableFromLink<TData = unknown>(\n query: DocumentNode,\n context: DefaultContext | undefined,\n variables?: OperationVariables,\n extensions?: Record<string, any>,\n // Prefer context.queryDeduplication if specified.\n deduplication: boolean = context?.queryDeduplication ??\n this.queryDeduplication\n ): {\n restart: () => void;\n observable: Observable<ApolloLink.Result<TData>>;\n } {\n let entry: {\n observable?: Observable<ApolloLink.Result<TData>>;\n // The restart function has to be on a mutable object that way if multiple\n // client.subscribe() calls are made before the first one subscribes to\n // the observable, the `restart` function can be updated for all\n // deduplicated client.subscribe() calls.\n restart?: () => void;\n } = {};\n\n const { serverQuery, clientQuery, operationType, hasIncrementalDirective } =\n this.getDocumentInfo(query);\n\n const operationName = getOperationName(query);\n const executeContext: ApolloLink.ExecuteContext = {\n client: this.client,\n };\n\n if (serverQuery) {\n const { inFlightLinkObservables, link } = this;\n\n try {\n const operation = this.incrementalHandler.prepareRequest({\n query: serverQuery,\n variables,\n context: {\n ...this.defaultContext,\n ...context,\n queryDeduplication: deduplication,\n },\n extensions,\n });\n\n context = operation.context;\n\n function withRestart(source: Observable<ApolloLink.Result>) {\n return new Observable<ApolloLink.Result>((observer) => {\n function subscribe() {\n return source.subscribe({\n next: observer.next.bind(observer),\n complete: observer.complete.bind(observer),\n error: observer.error.bind(observer),\n });\n }\n let subscription = subscribe();\n\n entry.restart ||= () => {\n subscription.unsubscribe();\n subscription = subscribe();\n };\n\n return () => {\n subscription.unsubscribe();\n entry.restart = undefined;\n };\n });\n }\n\n if (deduplication) {\n const printedServerQuery = print(serverQuery);\n const varJson = canonicalStringify(variables);\n\n entry = inFlightLinkObservables.lookup(printedServerQuery, varJson);\n\n if (!entry.observable) {\n entry.observable = execute(link, operation, executeContext).pipe(\n withRestart,\n finalize(() => {\n if (\n inFlightLinkObservables.peek(printedServerQuery, varJson) ===\n entry\n ) {\n inFlightLinkObservables.remove(printedServerQuery, varJson);\n }\n }),\n // We don't want to replay the last emitted value for\n // subscriptions and instead opt to wait to receive updates until\n // the subscription emits new values.\n operationType === OperationTypeNode.SUBSCRIPTION ?\n share()\n : shareReplay({ refCount: true })\n ) as Observable<ApolloLink.Result<TData>>;\n }\n } else {\n entry.observable = execute(link, operation, executeContext).pipe(\n withRestart\n ) as Observable<ApolloLink.Result<TData>>;\n }\n } catch (error) {\n entry.observable = throwError(() => error);\n }\n } else {\n entry.observable = of({ data: {} } as ApolloLink.Result<TData>);\n }\n\n if (clientQuery) {\n const { operation } = getOperationDefinition(query)!;\n if (__DEV__) {\n invariant(\n this.localState,\n \"%s '%s' contains `@client` fields but local state has not been configured.\",\n operation[0].toUpperCase() + operation.slice(1),\n operationName ?? \"(anonymous)\"\n );\n }\n\n invariant(\n !hasIncrementalDirective,\n \"%s '%s' contains `@client` and `@defer` directives. These cannot be used together.\",\n operation[0].toUpperCase() + operation.slice(1),\n operationName ?? \"(anonymous)\"\n );\n\n entry.observable = entry.observable.pipe(\n mergeMap((result) => {\n return from(\n this.localState!.execute<TData>({\n client: this.client,\n document: clientQuery,\n remoteResult: result as FormattedExecutionResult<TData>,\n context,\n variables,\n })\n );\n })\n );\n }\n\n return {\n restart: () => entry.restart?.(),\n observable: entry.observable.pipe(\n catchError((error) => {\n error = toErrorLike(error);\n registerLinkError(error);\n throw error;\n })\n ),\n };\n }\n\n private getResultsFromLink<TData, TVariables extends OperationVariables>(\n options: {\n query: DocumentNode;\n variables: TVariables;\n context: DefaultContext | undefined;\n fetchPolicy: WatchQueryFetchPolicy;\n errorPolicy: ErrorPolicy;\n },\n {\n queryInfo,\n cacheWriteBehavior,\n observableQuery,\n }: {\n queryInfo: QueryInfo<TData, TVariables>;\n cacheWriteBehavior: CacheWriteBehavior;\n observableQuery: ObservableQuery<TData, TVariables> | undefined;\n }\n ): Observable<ObservableQuery.Result<TData>> {\n const requestId = (queryInfo.lastRequestId = this.generateRequestId());\n const { errorPolicy } = options;\n\n // Performing transformForLink here gives this.cache a chance to fill in\n // missing fragment definitions (for example) before sending this document\n // through the link chain.\n const linkDocument = this.cache.transformForLink(options.query);\n\n return this.getObservableFromLink<TData>(\n linkDocument,\n options.context,\n options.variables\n ).observable.pipe(\n map((incoming) => {\n // Use linkDocument rather than queryInfo.document so the\n // operation/fragments used to write the result are the same as the\n // ones used to obtain it from the link.\n const result = queryInfo.markQueryResult(incoming, {\n ...options,\n document: linkDocument,\n cacheWriteBehavior,\n });\n const hasErrors = graphQLResultHasError(result);\n\n if (hasErrors && errorPolicy === \"none\") {\n queryInfo.resetLastWrite();\n observableQuery?.[\"resetNotifications\"]();\n throw new CombinedGraphQLErrors(result);\n }\n\n const aqr = {\n data: result.data as TData,\n ...(queryInfo.hasNext ?\n {\n loading: true,\n networkStatus: NetworkStatus.streaming,\n dataState: \"streaming\",\n partial: true,\n }\n : {\n dataState: result.data ? \"complete\" : \"empty\",\n loading: false,\n networkStatus: NetworkStatus.ready,\n partial: !result.data,\n }),\n } as ObservableQuery.Result<TData>;\n\n // In the case we start multiple network requests simultaneously, we\n // want to ensure we properly set `data` if we're reporting on an old\n // result which will not be caught by the conditional above that ends up\n // throwing the markError result.\n if (hasErrors) {\n if (errorPolicy === \"none\") {\n aqr.data = void 0 as TData;\n aqr.dataState = \"empty\";\n }\n if (errorPolicy !== \"ignore\") {\n aqr.error = new CombinedGraphQLErrors(result);\n if (aqr.dataState !== \"streaming\") {\n aqr.networkStatus = NetworkStatus.error;\n }\n }\n }\n\n return aqr;\n }),\n catchError((error) => {\n // Avoid storing errors from older interrupted queries.\n if (requestId >= queryInfo.lastRequestId && errorPolicy === \"none\") {\n queryInfo.resetLastWrite();\n observableQuery?.[\"resetNotifications\"]();\n throw error;\n }\n\n const aqr: ObservableQuery.Result<TData> = {\n data: undefined,\n dataState: \"empty\",\n loading: false,\n networkStatus: NetworkStatus.ready,\n partial: true,\n };\n\n if (errorPolicy !== \"ignore\") {\n aqr.error = error;\n aqr.networkStatus = NetworkStatus.error;\n }\n\n return of(aqr);\n })\n );\n }\n\n public fetchObservableWithInfo<TData, TVariables extends OperationVariables>(\n options: ApolloClient.WatchQueryOptions<TData, TVariables>,\n {\n // The initial networkStatus for this fetch, most often\n // NetworkStatus.loading, but also possibly fetchMore, poll, refetch,\n // or setVariables.\n networkStatus = NetworkStatus.loading,\n query = options.query,\n fetchQueryOperator = (x) => x,\n onCacheHit = () => {},\n observableQuery,\n }: {\n networkStatus?: NetworkStatus;\n query?: DocumentNode;\n fetchQueryOperator?: <T>(source: Observable<T>) => Observable<T>;\n onCacheHit?: () => void;\n observableQuery?: ObservableQuery<TData, TVariables> | undefined;\n }\n ): ObservableAndInfo<TData> {\n const variables = this.getVariables(query, options.variables) as TVariables;\n\n const defaults = this.defaultOptions.watchQuery;\n let {\n fetchPolicy = (defaults && defaults.fetchPolicy) || \"cache-first\",\n errorPolicy = (defaults && defaults.errorPolicy) || \"none\",\n returnPartialData = false,\n notifyOnNetworkStatusChange = true,\n context = {},\n } = options;\n\n if (\n this.prioritizeCacheValues &&\n (fetchPolicy === \"network-only\" || fetchPolicy === \"cache-and-network\")\n ) {\n fetchPolicy = \"cache-first\";\n }\n\n const normalized = Object.assign({}, options, {\n query,\n variables,\n fetchPolicy,\n errorPolicy,\n returnPartialData,\n notifyOnNetworkStatusChange,\n context,\n });\n\n const queryInfo = new QueryInfo<TData, TVariables>(this, observableQuery);\n\n const fromVariables = (variables: TVariables) => {\n // Since normalized is always a fresh copy of options, it's safe to\n // modify its properties here, rather than creating yet another new\n // WatchQueryOptions object.\n normalized.variables = variables;\n\n const cacheWriteBehavior =\n fetchPolicy === \"no-cache\" ? CacheWriteBehavior.FORBID\n // Watched queries must opt into overwriting existing data on refetch,\n // by passing refetchWritePolicy: \"overwrite\" in their WatchQueryOptions.\n : (\n networkStatus === NetworkStatus.refetch &&\n normalized.refetchWritePolicy !== \"merge\"\n ) ?\n CacheWriteBehavior.OVERWRITE\n : CacheWriteBehavior.MERGE;\n const observableWithInfo = this.fetchQueryByPolicy<TData, TVariables>(\n normalized,\n { queryInfo, cacheWriteBehavior, onCacheHit, observableQuery }\n );\n observableWithInfo.observable =\n observableWithInfo.observable.pipe(fetchQueryOperator);\n\n if (\n // If we're in standby, postpone advancing options.fetchPolicy using\n // applyNextFetchPolicy.\n normalized.fetchPolicy !== \"standby\"\n ) {\n observableQuery?.[\"applyNextFetchPolicy\"](\n \"after-fetch\",\n options as any\n );\n }\n\n return observableWithInfo;\n };\n\n // This cancel function needs to be set before the concast is created,\n // in case concast creation synchronously cancels the request.\n const cleanupCancelFn = () => {\n this.fetchCancelFns.delete(queryInfo.id);\n };\n this.fetchCancelFns.set(queryInfo.id, (error) => {\n fetchCancelSubject.next({\n kind: \"E\",\n error,\n source: \"network\",\n });\n });\n\n const fetchCancelSubject = new Subject<QueryNotification.Value<TData>>();\n let observable: Observable<QueryNotification.Value<TData>>,\n containsDataFromLink: boolean;\n\n // If the query has @export(as: ...) directives, then we need to\n // process those directives asynchronously. When there are no\n // @export directives (the common case), we deliberately avoid\n // wrapping the result of this.fetchQueryByPolicy in a Promise,\n // since the timing of result delivery is (unfortunately) important\n // for backwards compatibility. TODO This code could be simpler if\n // we deprecated and removed LocalState.\n if (this.getDocumentInfo(normalized.query).hasClientExports) {\n if (__DEV__) {\n invariant(\n this.localState,\n \"Query '%s' contains `@client` fields with variables provided by `@export` but local state has not been configured.\",\n getOperationName(normalized.query, \"(anonymous)\")\n );\n }\n\n observable = from(\n this.localState!.getExportedVariables({\n client: this.client,\n document: normalized.query,\n variables: normalized.variables,\n context: normalized.context,\n })\n ).pipe(mergeMap((variables) => fromVariables(variables).observable));\n\n // there is just no way we can synchronously get the *right* value here,\n // so we will assume `true`, which is the behaviour before the bug fix in\n // #10597. This means that bug is not fixed in that case, and is probably\n // un-fixable with reasonable effort for the edge case of @export as\n // directives.\n containsDataFromLink = true;\n } else {\n const sourcesWithInfo = fromVariables(normalized.variables);\n containsDataFromLink = sourcesWithInfo.fromLink;\n observable = sourcesWithInfo.observable;\n }\n\n return {\n // Merge `observable` with `fetchCancelSubject`, in a way that completing or\n // erroring either of them will complete the merged obserable.\n observable: new Observable<QueryNotification.Value<TData>>((observer) => {\n observer.add(cleanupCancelFn);\n observable.subscribe(observer);\n fetchCancelSubject.subscribe(observer);\n }).pipe(share()),\n fromLink: containsDataFromLink,\n };\n }\n\n public refetchQueries<TResult>({\n updateCache,\n include,\n optimistic = false,\n removeOptimistic = optimistic ? makeUniqueId(\"refetchQueries\") : void 0,\n onQueryUpdated,\n }: InternalRefetchQueriesOptions<\n ApolloCache,\n TResult\n >): InternalRefetchQueriesMap<TResult> {\n const includedQueriesByOq = new Map<\n ObservableQuery<any>,\n {\n oq: ObservableQuery<any>;\n lastDiff?: Cache.DiffResult<any>;\n diff?: Cache.DiffResult<any>;\n }\n >();\n\n if (include) {\n this.getObservableQueries(include).forEach((oq) => {\n if (oq.options.fetchPolicy === \"cache-only\" || oq[\"variablesUnknown\"]) {\n return;\n }\n\n const current = oq.getCurrentResult();\n includedQueriesByOq.set(oq, {\n oq,\n lastDiff: {\n result: current?.data,\n complete: !current?.partial,\n },\n });\n });\n }\n\n const results: InternalRefetchQueriesMap<TResult> = new Map();\n\n if (updateCache) {\n const handled = new Set<ObservableQuery<any>>();\n this.cache.batch({\n update: updateCache,\n\n // Since you can perform any combination of cache reads and/or writes in\n // the cache.batch update function, its optimistic option can be either\n // a boolean or a string, representing three distinct modes of\n // operation:\n //\n // * false: read/write only the root layer\n // * true: read/write the topmost layer\n // * string: read/write a fresh optimistic layer with that ID string\n //\n // When typeof optimistic === \"string\", a new optimistic layer will be\n // temporarily created within cache.batch with that string as its ID. If\n // we then pass that same string as the removeOptimistic option, we can\n // make cache.batch immediately remove the optimistic layer after\n // running the updateCache function, triggering only one broadcast.\n //\n // However, the refetchQueries method accepts only true or false for its\n // optimistic option (not string). We interpret true to mean a temporary\n // optimistic layer should be created, to allow efficiently rolling back\n // the effect of the updateCache function, which involves passing a\n // string instead of true as the optimistic option to cache.batch, when\n // refetchQueries receives optimistic: true.\n //\n // In other words, we are deliberately not supporting the use case of\n // writing to an *existing* optimistic layer (using the refetchQueries\n // updateCache function), since that would potentially interfere with\n // other optimistic updates in progress. Instead, you can read/write\n // only the root layer by passing optimistic: false to refetchQueries,\n // or you can read/write a brand new optimistic layer that will be\n // automatically removed by passing optimistic: true.\n optimistic: (optimistic && removeOptimistic) || false,\n\n // The removeOptimistic option can also be provided by itself, even if\n // optimistic === false, to remove some previously-added optimistic\n // layer safely and efficiently, like we do in markMutationResult.\n //\n // If an explicit removeOptimistic string is provided with optimistic:\n // true, the removeOptimistic string will determine the ID of the\n // temporary optimistic layer, in case that ever matters.\n removeOptimistic,\n\n onWatchUpdated(watch, diff, lastDiff) {\n const oq = watch.watcher;\n\n if (oq instanceof ObservableQuery && !handled.has(oq)) {\n handled.add(oq);\n if (onQueryUpdated) {\n // Since we're about to handle this query now, remove it from\n // includedQueriesById, in case it was added earlier because of\n // options.include.\n includedQueriesByOq.delete(oq);\n\n let result:\n | TResult\n | boolean\n | Promise<ApolloClient.QueryResult<any>> = onQueryUpdated(\n oq,\n diff,\n lastDiff\n );\n\n if (result === true) {\n // The onQueryUpdated function requested the default refetching\n // behavior by returning true.\n result = oq\n .refetch()\n .retain(/* create a persistent subscription on the query */);\n }\n\n // Record the result in the results Map, as long as onQueryUpdated\n // did not return false to skip/ignore this result.\n if (result !== false) {\n results.set(\n oq,\n result as InternalRefetchQueriesResult<TResult>\n );\n }\n\n // Allow the default cache broadcast to happen, except when\n // onQueryUpdated returns false.\n return result;\n }\n\n if (\n onQueryUpdated !== null &&\n oq.options.fetchPolicy !== \"cache-only\"\n ) {\n // If we don't have an onQueryUpdated function, and onQueryUpdated\n // was not disabled by passing null, make sure this query is\n // \"included\" like any other options.include-specified query.\n includedQueriesByOq.set(oq, { oq, lastDiff, diff });\n }\n }\n },\n });\n }\n\n if (includedQueriesByOq.size) {\n includedQueriesByOq.forEach(({ oq, lastDiff, diff }) => {\n let result:\n | TResult\n | boolean\n | Promise<ApolloClient.QueryResult<any>>\n | undefined;\n\n // If onQueryUpdated is provided, we want to use it for all included\n // queries, even the QueryOptions ones.\n if (onQueryUpdated) {\n if (!diff) {\n diff = oq.getCacheDiff();\n }\n result = onQueryUpdated(oq, diff, lastDiff);\n }\n\n // Otherwise, we fall back to refetching.\n if (!onQueryUpdated || result === true) {\n result = oq\n .refetch()\n .retain(/* create a persistent subscription on the query */);\n }\n\n if (result !== false) {\n results.set(oq, result as InternalRefetchQueriesResult<TResult>);\n }\n });\n }\n\n if (removeOptimistic) {\n // In case no updateCache callback was provided (so cache.batch was not\n // called above, and thus did not already remove the optimistic layer),\n // remove it here. Since this is a no-op when the layer has already been\n // removed, we do it even if we called cache.batch above, since it's\n // possible this.cache is an instance of some ApolloCache subclass other\n // than InMemoryCache, and does not fully support the removeOptimistic\n // option for cache.batch.\n this.cache.removeOptimistic(removeOptimistic);\n }\n\n return results;\n }\n\n private noCacheWarningsByCause = new WeakSet<object>();\n\n public maskOperation<TData = unknown>(\n options: MaskOperationOptions<TData>\n ): MaybeMasked<TData> {\n const { document, data } = options;\n\n if (__DEV__) {\n const { fetchPolicy, cause = {} } = options;\n const operationType = getOperationDefinition(document)?.operation;\n\n if (\n this.dataMasking &&\n fetchPolicy === \"no-cache\" &&\n !isFullyUnmaskedOperation(document) &&\n !this.noCacheWarningsByCause.has(cause)\n )