UNPKG

@apollo/client

Version:

A fully-featured caching GraphQL client.

1,177 lines 54 kB
import { Trie } from "@wry/trie";
import { BREAK, Kind, OperationTypeNode, visit } from "graphql";
import { Observable, throwError } from "rxjs";
import { catchError, concat, EMPTY, filter, finalize, from, lastValueFrom, map, materialize, mergeMap, of, share, shareReplay, Subject, tap, } from "rxjs";
import { canonicalStringify } from "@apollo/client/cache";
import { CombinedGraphQLErrors, graphQLResultHasProtocolErrors, registerLinkError, toErrorLike, } from "@apollo/client/errors";
import { PROTOCOL_ERRORS_SYMBOL } from "@apollo/client/errors";
import { execute } from "@apollo/client/link";
import { maskFragment, maskOperation } from "@apollo/client/masking";
import { cacheSizes, DocumentTransform, isNetworkRequestInFlight, print, } from "@apollo/client/utilities";
import { __DEV__ } from "@apollo/client/utilities/environment";
import { AutoCleanedWeakCache, checkDocument, coerceScalarFieldsToParsed, extensionsSymbol, filterMap, getDefaultValues, getOperationDefinition, getOperationName, graphQLResultHasError, hasDirectives, hasForcedResolvers, isDocumentNode, isNonNullObject, makeUniqueId, mergeOptions, removeDirectivesFromDocument, streamInfoSymbol, toQueryResult, } from "@apollo/client/utilities/internal";
import { invariant, newInvariantError, } from "@apollo/client/utilities/invariant";
import { dataStateErrorCache } from "./dataStateErrorCache.js";
import { NetworkStatus } from "./networkStatus.js";
import { logMissingFieldErrors, ObservableQuery } from "./ObservableQuery.js";
import { QueryInfo } from "./QueryInfo.js";
export class QueryManager {
    defaultOptions;
    client;
    /**
     * The options that were passed to the ApolloClient constructor.
     */
    clientOptions;
    assumeImmutableResults;
    documentTransform;
    ssrMode;
    defaultContext;
    dataMasking;
    incrementalHandler;
    localState;
    queryDeduplication;
    /**
     * Whether to prioritize cache values over network results when
     * `fetchObservableWithInfo` is called.
     * This will essentially turn a `"network-only"` or `"cache-and-network"`
     * fetchPolicy into a `"cache-first"` fetchPolicy, but without influencing
     * the `fetchPolicy` of the `ObservableQuery`.
     *
     * This can e.g. be used to prioritize the cache during the first render after
     * SSR.
     */
    prioritizeCacheValues = false;
    onBroadcast;
    mutationStore;
    /**
     * All ObservableQueries that currently have at least one subscriber.
     */
    obsQueries = new Set();
    // Maps from queryInfo.id strings to Promise rejection functions for
    // currently active queries and fetches.
    // Use protected instead of private field so
    // @apollo/experimental-nextjs-app-support can access type info.
    fetchCancelFns = new Map();
    constructor(options) {
        const defaultDocumentTransform = new DocumentTransform((document) => this.cache.transformDocument(document), 
        // Allow the apollo cache to manage its own transform caches
        { cache: false });
        this.client = options.client;
        this.defaultOptions = options.defaultOptions;
        this.queryDeduplication = options.queryDeduplication;
        this.clientOptions = options.clientOptions;
        this.ssrMode = options.ssrMode;
        this.assumeImmutableResults = options.assumeImmutableResults;
        this.dataMasking = options.dataMasking;
        this.localState = options.localState;
        this.incrementalHandler = options.incrementalHandler;
        const documentTransform = options.documentTransform;
        this.documentTransform =
            documentTransform ?
                defaultDocumentTransform
                    .concat(documentTransform)
                    // The custom document transform may add new fragment spreads or new
                    // field selections, so we want to give the cache a chance to run
                    // again. For example, the InMemoryCache adds __typename to field
                    // selections and fragments from the fragment registry.
                    .concat(defaultDocumentTransform)
                : defaultDocumentTransform;
        // Put the incremental transform last so custom document transforms don't
        // revert changes made to the document accidentally. Adding last also
        // ensures `@defer` fragments added by the fragment registry are labeled.
        if (this.incrementalHandler.documentTransform) {
            this.documentTransform = this.documentTransform.concat(this.incrementalHandler.documentTransform);
        }
        this.defaultContext = options.defaultContext || {};
        if ((this.onBroadcast = options.onBroadcast)) {
            this.mutationStore = {};
        }
    }
    get link() {
        return this.client.link;
    }
    get cache() {
        return this.client.cache;
    }
    /**
     * Call this method to terminate any active query processes, making it safe
     * to dispose of this QueryManager instance.
     */
    stop() {
        this.obsQueries.forEach((oq) => oq.stop());
        this.cancelPendingFetches(newInvariantError(92));
    }
    cancelPendingFetches(error) {
        this.fetchCancelFns.forEach((cancel) => cancel(error));
        this.fetchCancelFns.clear();
    }
    async mutate({ mutation, variables, optimisticResponse, updateQueries, refetchQueries = [], awaitRefetchQueries = false, update: updateWithProxyFn, onQueryUpdated, fetchPolicy, errorPolicy, keepRootFields, context, }) {
        const queryInfo = new QueryInfo(this);
        mutation = this.cache.transformForLink(this.transform(mutation));
        const { hasClientExports } = this.getDocumentInfo(mutation);
        variables = this.getVariables(mutation, variables);
        if (hasClientExports) {
            if (__DEV__) {
                invariant(this.localState, 93, getOperationName(mutation, "(anonymous)"));
            }
            variables = await this.localState.getExportedVariables({
                client: this.client,
                document: mutation,
                variables,
                context,
            });
        }
        const mutationStoreValue = this.mutationStore &&
            (this.mutationStore[queryInfo.id] = {
                mutation,
                variables: this.cache.serializeVariables(mutation, variables),
                loading: true,
                error: null,
            });
        const isOptimistic = optimisticResponse &&
            queryInfo.markMutationOptimistic(optimisticResponse, {
                document: mutation,
                variables,
                cacheWriteBehavior: fetchPolicy === "no-cache" ?
                    0 /* CacheWriteBehavior.FORBID */
                    : 2 /* CacheWriteBehavior.MERGE */,
                errorPolicy,
                context,
                updateQueries,
                update: updateWithProxyFn,
                keepRootFields,
            });
        this.broadcastQueries();
        return new Promise((resolve, reject) => {
            const cause = {};
            return this.getObservableFromLink(mutation, {
                ...context,
                optimisticResponse: isOptimistic ? optimisticResponse : void 0,
            }, variables, fetchPolicy, {}, false)
                .observable.pipe(validateDidEmitValue(), mergeMap((result) => {
                const storeResult = { ...result };
                return from(queryInfo.markMutationResult(storeResult, {
                    document: mutation,
                    variables,
                    cacheWriteBehavior: fetchPolicy === "no-cache" ?
                        0 /* CacheWriteBehavior.FORBID */
                        : 2 /* CacheWriteBehavior.MERGE */,
                    errorPolicy,
                    context,
                    update: updateWithProxyFn,
                    updateQueries,
                    awaitRefetchQueries,
                    refetchQueries,
                    removeOptimistic: isOptimistic ? queryInfo.id : void 0,
                    onQueryUpdated,
                    keepRootFields,
                }));
            }))
                .pipe(map((storeResult) => {
                const hasErrors = graphQLResultHasError(storeResult);
                if (hasErrors && errorPolicy === "none") {
                    throw new CombinedGraphQLErrors(removeStreamDetailsFromExtensions(storeResult));
                }
                if (mutationStoreValue) {
                    mutationStoreValue.loading = false;
                    mutationStoreValue.error = null;
                }
                return storeResult;
            }))
                .subscribe({
                next: (storeResult) => {
                    this.broadcastQueries();
                    // Since mutations might receive multiple payloads from the
                    // ApolloLink chain (e.g. when used with @defer),
                    // we resolve with a SingleExecutionResult or after the final
                    // ExecutionPatchResult has arrived and we have assembled the
                    // multipart response into a single result.
                    if (!queryInfo.hasNext) {
                        const result = {
                            data: this.maskOperation({
                                document: mutation,
                                data: storeResult.data,
                                fetchPolicy,
                                cause,
                            }),
                        };
                        if (graphQLResultHasError(storeResult)) {
                            result.error = new CombinedGraphQLErrors(storeResult);
                        }
                        if (Object.keys(storeResult.extensions || {}).length) {
                            result.extensions = storeResult.extensions;
                        }
                        resolve(result);
                    }
                },
                error: (error) => {
                    if (mutationStoreValue) {
                        mutationStoreValue.loading = false;
                        mutationStoreValue.error = error;
                    }
                    if (isOptimistic) {
                        this.cache.removeOptimistic(queryInfo.id);
                    }
                    this.broadcastQueries();
                    if (errorPolicy === "ignore") {
                        return resolve({ data: undefined });
                    }
                    if (errorPolicy === "all") {
                        return resolve({ data: undefined, error });
                    }
                    reject(error);
                },
            });
        });
    }
    fetchQuery(options, networkStatus) {
        checkDocument(options.query, OperationTypeNode.QUERY);
        // do the rest asynchronously to keep the same rejection timing as
        // checks further in `.mutate`
        return (async () => lastValueFrom(this.fetchObservableWithInfo(options, {
            networkStatus,
        }).observable.pipe(filterMap((value) => {
            switch (value.kind) {
                case "E":
                    throw value.error;
                case "N": {
                    if (value.source !== "newNetworkStatus")
                        return toQueryResult(value.value);
                }
            }
        })), {
            // This default is needed when a `standby` fetch policy is used to avoid
            // an EmptyError from rejecting this promise.
            defaultValue: { data: undefined },
        }))();
    }
    transform(document) {
        return this.documentTransform.transformDocument(document);
    }
    transformCache = new AutoCleanedWeakCache(cacheSizes["queryManager.getDocumentInfo"] ||
        2000 /* defaultCacheSizes["queryManager.getDocumentInfo"] */);
    getDocumentInfo(document) {
        const { transformCache } = this;
        if (!transformCache.has(document)) {
            const operationDefinition = getOperationDefinition(document);
            const cacheEntry = {
                // TODO These three calls (hasClientExports, shouldForceResolvers, and
                // usesNonreactiveDirective) are performing independent full traversals
                // of the transformed document. We should consider merging these
                // traversals into a single pass in the future, though the work is
                // cached after the first time.
                hasClientExports: hasDirectives(["client", "export"], document, true),
                hasForcedResolvers: hasForcedResolvers(document),
                hasNonreactiveDirective: hasDirectives(["nonreactive"], document),
                hasIncrementalDirective: hasDirectives(["defer"], document),
                nonReactiveQuery: addNonReactiveToNamedFragments(document),
                clientQuery: hasDirectives(["client"], document) ? document : null,
                serverQuery: removeDirectivesFromDocument([
                    { name: "client", remove: true },
                    { name: "connection" },
                    { name: "nonreactive" },
                    { name: "unmask" },
                ], document),
                operationType: operationDefinition?.operation,
                defaultVars: getDefaultValues(operationDefinition),
                // Transform any mutation or subscription operations to query operations
                // so we can read/write them from/to the cache.
                asQuery: {
                    ...document,
                    definitions: document.definitions.map((def) => {
                        if (def.kind === "OperationDefinition" &&
                            def.operation !== "query") {
                            return { ...def, operation: "query" };
                        }
                        return def;
                    }),
                },
            };
            transformCache.set(document, cacheEntry);
        }
        const entry = transformCache.get(document);
        if (entry.violation) {
            throw entry.violation;
        }
        return entry;
    }
    getVariables(document, variables) {
        const defaultVars = this.getDocumentInfo(document).defaultVars;
        const varsWithDefaults = Object.entries(variables ?? {}).map(([key, value]) => [key, value === undefined ? defaultVars[key] : value]);
        return {
            ...defaultVars,
            ...Object.fromEntries(varsWithDefaults),
        };
    }
    watchQuery(options) {
        checkDocument(options.query, OperationTypeNode.QUERY);
        const query = this.transform(options.query);
        // assign variable default values if supplied
        // NOTE: We don't modify options.query here with the transformed query to
        // ensure observable.options.query is set to the raw untransformed query.
        options = {
            ...options,
            variables: this.getVariables(query, options.variables),
        };
        if (typeof options.notifyOnNetworkStatusChange === "undefined") {
            options.notifyOnNetworkStatusChange = true;
        }
        const observable = new ObservableQuery({
            queryManager: this,
            options,
            transformedQuery: query,
        });
        return observable;
    }
    query(options) {
        const query = this.transform(options.query);
        return this.fetchQuery({
            ...options,
            query,
        }).then((value) => ({
            ...value,
            data: this.maskOperation({
                document: query,
                data: value?.data,
                fetchPolicy: options.fetchPolicy,
            }),
        }));
    }
    clearStore(options = {
        discardWatches: true,
    }) {
        // Before we have sent the reset action to the store, we can no longer
        // rely on the results returned by in-flight requests since these may
        // depend on values that previously existed in the data portion of the
        // store. So, we cancel the promises and observers that we have issued
        // so far and not yet resolved (in the case of queries).
        this.cancelPendingFetches(newInvariantError(94));
        this.obsQueries.forEach((observableQuery) => {
            // Set loading to true so listeners don't trigger unless they want
            // results with partial data.
            observableQuery.reset();
        });
        if (this.mutationStore) {
            this.mutationStore = {};
        }
        // begin removing data from the store
        return this.cache.reset(options);
    }
    getObservableQueries(include = "active") {
        const queries = new Set();
        const queryNames = new Map();
        const queryNamesAndQueryStrings = new Map();
        const legacyQueryOptions = new Set();
        if (Array.isArray(include)) {
            include.forEach((desc) => {
                if (typeof desc === "string") {
                    queryNames.set(desc, desc);
                    queryNamesAndQueryStrings.set(desc, false);
                }
                else if (isDocumentNode(desc)) {
                    const queryString = print(this.transform(desc));
                    queryNames.set(queryString, getOperationName(desc));
                    queryNamesAndQueryStrings.set(queryString, false);
                }
                else if (isNonNullObject(desc) && desc.query) {
                    legacyQueryOptions.add(desc);
                }
            });
        }
        this.obsQueries.forEach((oq) => {
            const document = print(this.transform(oq.options.query));
            if (include === "all") {
                queries.add(oq);
                return;
            }
            const { queryName, options: { fetchPolicy }, } = oq;
            if (include === "active" && fetchPolicy === "standby") {
                return;
            }
            if (include === "active" ||
                (queryName && queryNamesAndQueryStrings.has(queryName)) ||
                (document && queryNamesAndQueryStrings.has(document))) {
                queries.add(oq);
                if (queryName)
                    queryNamesAndQueryStrings.set(queryName, true);
                if (document)
                    queryNamesAndQueryStrings.set(document, true);
            }
        });
        if (legacyQueryOptions.size) {
            legacyQueryOptions.forEach((options) => {
                const oq = new ObservableQuery({
                    queryManager: this,
                    options: {
                        ...mergeOptions(this.defaultOptions.watchQuery, options),
                        fetchPolicy: "network-only",
                    },
                });
                queries.add(oq);
            });
        }
        if (__DEV__ && queryNamesAndQueryStrings.size) {
            queryNamesAndQueryStrings.forEach((included, nameOrQueryString) => {
                if (!included) {
                    const queryName = queryNames.get(nameOrQueryString);
                    if (queryName) {
                        __DEV__ && invariant.warn(95, queryName);
                    }
                    else {
                        __DEV__ && invariant.warn(96);
                    }
                }
            });
        }
        return queries;
    }
    refetchObservableQueries(includeStandby = false) {
        const observableQueryPromises = [];
        this.getObservableQueries(includeStandby ? "all" : "active").forEach((observableQuery) => {
            const { fetchPolicy } = observableQuery.options;
            if ((includeStandby || fetchPolicy !== "standby") &&
                fetchPolicy !== "cache-only") {
                observableQueryPromises.push(observableQuery.refetch());
            }
        });
        this.broadcastQueries();
        return Promise.all(observableQueryPromises);
    }
    startGraphQLSubscription(options) {
        let { query, variables } = options;
        const { fetchPolicy = "cache-first", errorPolicy = "none", context = {}, extensions = {}, } = options;
        checkDocument(query, OperationTypeNode.SUBSCRIPTION);
        query = this.transform(query);
        variables = this.getVariables(query, variables);
        let restart;
        if (__DEV__) {
            invariant(
                !this.getDocumentInfo(query).hasClientExports || this.localState,
                97,
                getOperationName(query, "(anonymous)")
            );
        }
        const observable = (this.getDocumentInfo(query).hasClientExports ?
            from(this.localState.getExportedVariables({
                client: this.client,
                document: query,
                variables,
                context,
            }))
            : of(variables)).pipe(mergeMap((variables) => {
            const { observable, restart: res } = this.getObservableFromLink(query, context, variables, fetchPolicy, extensions);
            const queryInfo = new QueryInfo(this);
            restart = res;
            return observable.pipe(map((rawResult) => {
                queryInfo.markSubscriptionResult(rawResult, {
                    document: query,
                    variables,
                    errorPolicy,
                    cacheWriteBehavior: fetchPolicy === "no-cache" ?
                        0 /* CacheWriteBehavior.FORBID */
                        : 2 /* CacheWriteBehavior.MERGE */,
                });
                const result = {
                    data: rawResult.data ?? undefined,
                };
                if (graphQLResultHasError(rawResult)) {
                    result.error = new CombinedGraphQLErrors(rawResult);
                }
                else if (graphQLResultHasProtocolErrors(rawResult)) {
                    result.error = rawResult.extensions[PROTOCOL_ERRORS_SYMBOL];
                    // Don't emit protocol errors added by HttpLink
                    delete rawResult.extensions[PROTOCOL_ERRORS_SYMBOL];
                }
                if (rawResult.extensions &&
                    Object.keys(rawResult.extensions).length) {
                    result.extensions = rawResult.extensions;
                }
                if (result.error && errorPolicy === "none") {
                    result.data = undefined;
                }
                if (errorPolicy === "ignore") {
                    delete result.error;
                }
                return result;
            }), catchError((error) => {
                if (errorPolicy === "ignore") {
                    return of({
                        data: undefined,
                    });
                }
                return of({ data: undefined, error });
            }), filter((result) => !!(result.data || result.error)));
        }));
        return Object.assign(observable, { restart: () => restart?.() });
    }
    broadcastQueries() {
        if (this.onBroadcast)
            this.onBroadcast();
        this.obsQueries.forEach((observableQuery) => observableQuery.notify());
    }
    // Use protected instead of private field so
    // @apollo/experimental-nextjs-app-support can access type info.
    inFlightLinkObservables = new Trie(false);
    getObservableFromLink(query, context, variables, fetchPolicy, extensions, 
    // Prefer context.queryDeduplication if specified.
    deduplication = context?.queryDeduplication ??
        this.queryDeduplication) {
        let entry = {};
        const { serverQuery, clientQuery, operationType, hasIncrementalDirective } = this.getDocumentInfo(query);
        const operationName = getOperationName(query);
        const executeContext = {
            client: this.client,
        };
        variables = this.cache.serializeVariables(query, variables);
        if (serverQuery) {
            const { inFlightLinkObservables, link } = this;
            try {
                const operation = this.incrementalHandler.prepareRequest({
                    query: serverQuery,
                    variables,
                    context: {
                        ...this.defaultContext,
                        ...context,
                        queryDeduplication: deduplication,
                    },
                    extensions,
                });
                context = operation.context;
                function withRestart(source) {
                    return new Observable((observer) => {
                        function subscribe() {
                            return source.subscribe({
                                next: observer.next.bind(observer),
                                complete: observer.complete.bind(observer),
                                error: observer.error.bind(observer),
                            });
                        }
                        let subscription = subscribe();
                        entry.restart ||= () => {
                            subscription.unsubscribe();
                            subscription = subscribe();
                        };
                        return () => {
                            subscription.unsubscribe();
                            entry.restart = undefined;
                        };
                    });
                }
                if (deduplication) {
                    const printedServerQuery = print(serverQuery);
                    const varJson = canonicalStringify(variables);
                    entry = inFlightLinkObservables.lookup(printedServerQuery, varJson);
                    if (!entry.observable) {
                        entry.observable = execute(link, operation, executeContext).pipe(withRestart, finalize(() => {
                            if (inFlightLinkObservables.peek(printedServerQuery, varJson) ===
                                entry) {
                                inFlightLinkObservables.remove(printedServerQuery, varJson);
                            }
                        }), 
                        // We don't want to replay the last emitted value for
                        // subscriptions and instead opt to wait to receive updates until
                        // the subscription emits new values.
                        operationType === OperationTypeNode.SUBSCRIPTION ?
                            share()
                            : shareReplay({ refCount: true }));
                    }
                }
                else {
                    entry.observable = execute(link, operation, executeContext).pipe(withRestart);
                }
            }
            catch (error) {
                entry.observable = throwError(() => error);
            }
        }
        else {
            entry.observable = of({ data: {} });
        }
        if (clientQuery) {
            const { operation } = getOperationDefinition(query);
            if (__DEV__) {
                invariant(
                    this.localState,
                    98,
                    operation[0].toUpperCase() + operation.slice(1),
                    operationName ?? "(anonymous)"
                );
            }
            invariant(
                !hasIncrementalDirective,
                99,
                operation[0].toUpperCase() + operation.slice(1),
                operationName ?? "(anonymous)"
            );
            entry.observable = entry.observable.pipe(mergeMap((result) => {
                return from(this.localState.execute({
                    client: this.client,
                    document: clientQuery,
                    remoteResult: result,
                    context,
                    variables,
                    fetchPolicy,
                }));
            }));
        }
        return {
            restart: () => entry.restart?.(),
            observable: entry.observable.pipe(catchError((error) => {
                error = toErrorLike(error);
                registerLinkError(error);
                throw error;
            })),
        };
    }
    getResultsFromLink(options, { queryInfo, cacheWriteBehavior, observableQuery, exposeExtensions, prunePendingDeferFragments, }) {
        const { errorPolicy } = options;
        // Performing transformForLink here gives this.cache a chance to fill in
        // missing fragment definitions (for example) before sending this document
        // through the link chain.
        const linkDocument = this.cache.transformForLink(options.query);
        return this.getObservableFromLink(linkDocument, options.context, options.variables, options.fetchPolicy).observable.pipe(map((incoming) => {
            // Use linkDocument rather than queryInfo.document so the
            // operation/fragments used to write the result are the same as the
            // ones used to obtain it from the link.
            const { dataState, ...result } = queryInfo.markQueryResult(incoming, {
                ...options,
                document: linkDocument,
                cacheWriteBehavior,
                returnPartialData: options.returnPartialData,
                prunePendingDeferFragments,
            });
            const hasErrors = graphQLResultHasError(result);
            if (hasErrors && errorPolicy === "none") {
                observableQuery?.["resetNotifications"]();
                const error = new CombinedGraphQLErrors(removeStreamDetailsFromExtensions(result));
                dataStateErrorCache.set(error, dataState);
                throw error;
            }
            const partial = dataState !== "complete";
            const aqr = {
                data: result.data,
                ...(queryInfo.hasNext ?
                    {
                        loading: true,
                        networkStatus: NetworkStatus.streaming,
                        dataState,
                        partial,
                    }
                    : {
                        dataState,
                        loading: false,
                        networkStatus: NetworkStatus.ready,
                        partial,
                    }),
            };
            if (exposeExtensions && "extensions" in result) {
                aqr[extensionsSymbol] = result.extensions;
            }
            if (hasErrors && errorPolicy !== "ignore") {
                aqr.error = new CombinedGraphQLErrors(removeStreamDetailsFromExtensions(result));
                if (aqr.networkStatus !== NetworkStatus.streaming) {
                    aqr.networkStatus = NetworkStatus.error;
                }
            }
            return aqr;
        }), catchError((error) => {
            if (errorPolicy === "none") {
                observableQuery?.["resetNotifications"]();
                throw error;
            }
            const aqr = {
                data: undefined,
                dataState: "empty",
                loading: false,
                networkStatus: NetworkStatus.ready,
                partial: true,
            };
            if (errorPolicy !== "ignore") {
                aqr.error = error;
                aqr.networkStatus = NetworkStatus.error;
            }
            return of(aqr);
        }));
    }
    fetchObservableWithInfo(options, { 
    // The initial networkStatus for this fetch, most often
    // NetworkStatus.loading, but also possibly fetchMore, poll, refetch,
    // or setVariables.
    networkStatus = NetworkStatus.loading, query = options.query, fetchQueryOperator = (x) => x, onCacheHit = () => { }, observableQuery, exposeExtensions, }) {
        const variables = this.getVariables(query, options.variables);
        let { fetchPolicy = "cache-first", errorPolicy = "none", returnPartialData = false, notifyOnNetworkStatusChange = true, context = {}, } = options;
        if (this.prioritizeCacheValues &&
            (fetchPolicy === "network-only" || fetchPolicy === "cache-and-network")) {
            fetchPolicy = "cache-first";
        }
        const normalized = Object.assign({}, options, {
            query,
            variables,
            fetchPolicy,
            errorPolicy,
            returnPartialData,
            networkStatus,
            notifyOnNetworkStatusChange,
            context,
        });
        const queryInfo = new QueryInfo(this, observableQuery);
        const fromVariables = (variables) => {
            // Since normalized is always a fresh copy of options, it's safe to
            // modify its properties here, rather than creating yet another new
            // WatchQueryOptions object.
            normalized.variables = variables;
            const cacheWriteBehavior = fetchPolicy === "no-cache" ? 0 /* CacheWriteBehavior.FORBID */
                // Watched queries must opt into overwriting existing data on refetch,
                // by passing refetchWritePolicy: "overwrite" in their WatchQueryOptions.
                : (networkStatus === NetworkStatus.refetch &&
                    normalized.refetchWritePolicy !== "merge") ?
                    1 /* CacheWriteBehavior.OVERWRITE */
                    : 2 /* CacheWriteBehavior.MERGE */;
            const observableWithInfo = this.fetchQueryByPolicy(normalized, {
                queryInfo,
                cacheWriteBehavior,
                onCacheHit,
                observableQuery,
                exposeExtensions,
            });
            observableWithInfo.observable =
                observableWithInfo.observable.pipe(fetchQueryOperator);
            if (
            // If we're in standby, postpone advancing options.fetchPolicy using
            // applyNextFetchPolicy.
            normalized.fetchPolicy !== "standby") {
                observableQuery?.["applyNextFetchPolicy"]("after-fetch", options);
            }
            return observableWithInfo;
        };
        // 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(queryInfo.id);
        };
        this.fetchCancelFns.set(queryInfo.id, (error) => {
            fetchCancelSubject.next({
                kind: "E",
                error,
                source: "network",
            });
        });
        const fetchCancelSubject = new Subject();
        let observable, containsDataFromLink;
        // If the query has @export(as: ...) directives, then we need to
        // process those directives asynchronously. When there are no
        // @export directives (the common case), we deliberately avoid
        // wrapping the result of this.fetchQueryByPolicy in a Promise,
        // since the timing of result delivery is (unfortunately) important
        // for backwards compatibility. TODO This code could be simpler if
        // we deprecated and removed LocalState.
        if (this.getDocumentInfo(normalized.query).hasClientExports) {
            if (__DEV__) {
                invariant(this.localState, 100, getOperationName(normalized.query, "(anonymous)"));
            }
            observable = from(this.localState.getExportedVariables({
                client: this.client,
                document: normalized.query,
                variables: normalized.variables,
                context: normalized.context,
            })).pipe(mergeMap((variables) => fromVariables(variables).observable));
            // there is just no way we can synchronously get the *right* value here,
            // so we will assume `true`, which is the behaviour before the bug fix in
            // #10597. This means that bug is not fixed in that case, and is probably
            // un-fixable with reasonable effort for the edge case of @export as
            // directives.
            containsDataFromLink = true;
        }
        else {
            const sourcesWithInfo = fromVariables(normalized.variables);
            containsDataFromLink = sourcesWithInfo.fromLink;
            observable = sourcesWithInfo.observable;
        }
        return {
            // Merge `observable` with `fetchCancelSubject`, in a way that completing or
            // erroring either of them will complete the merged obserable.
            observable: new Observable((observer) => {
                observer.add(cleanupCancelFn);
                observable.subscribe(observer);
                fetchCancelSubject.subscribe(observer);
            }).pipe(share()),
            fromLink: containsDataFromLink,
        };
    }
    refetchQueries({ updateCache, include, optimistic = false, removeOptimistic = optimistic ? makeUniqueId("refetchQueries") : void 0, onQueryUpdated, }) {
        const includedQueriesByOq = new Map();
        if (include) {
            this.getObservableQueries(include).forEach((oq) => {
                if (oq.options.fetchPolicy === "cache-only" || oq["variablesUnknown"]) {
                    return;
                }
                const current = oq.getCurrentResult();
                includedQueriesByOq.set(oq, {
                    oq,
                    lastDiff: {
                        result: current?.data,
                        complete: !current?.partial,
                    },
                });
            });
        }
        const results = new Map();
        if (updateCache) {
            const handled = new Set();
            this.cache.batch({
                update: updateCache,
                // Since you can perform any combination of cache reads and/or writes in
                // the cache.batch update function, its optimistic option can be either
                // a boolean or a string, representing three distinct modes of
                // operation:
                //
                // * false: read/write only the root layer
                // * true: read/write the topmost layer
                // * string: read/write a fresh optimistic layer with that ID string
                //
                // When typeof optimistic === "string", a new optimistic layer will be
                // temporarily created within cache.batch with that string as its ID. If
                // we then pass that same string as the removeOptimistic option, we can
                // make cache.batch immediately remove the optimistic layer after
                // running the updateCache function, triggering only one broadcast.
                //
                // However, the refetchQueries method accepts only true or false for its
                // optimistic option (not string). We interpret true to mean a temporary
                // optimistic layer should be created, to allow efficiently rolling back
                // the effect of the updateCache function, which involves passing a
                // string instead of true as the optimistic option to cache.batch, when
                // refetchQueries receives optimistic: true.
                //
                // In other words, we are deliberately not supporting the use case of
                // writing to an *existing* optimistic layer (using the refetchQueries
                // updateCache function), since that would potentially interfere with
                // other optimistic updates in progress. Instead, you can read/write
                // only the root layer by passing optimistic: false to refetchQueries,
                // or you can read/write a brand new optimistic layer that will be
                // automatically removed by passing optimistic: true.
                optimistic: (optimistic && removeOptimistic) || false,
                // The removeOptimistic option can also be provided by itself, even if
                // optimistic === false, to remove some previously-added optimistic
                // layer safely and efficiently, like we do in markMutationResult.
                //
                // If an explicit removeOptimistic string is provided with optimistic:
                // true, the removeOptimistic string will determine the ID of the
                // temporary optimistic layer, in case that ever matters.
                removeOptimistic,
                onWatchUpdated(watch, diff, lastDiff) {
                    const oq = watch.watcher;
                    if (oq instanceof ObservableQuery && !handled.has(oq)) {
                        handled.add(oq);
                        if (onQueryUpdated) {
                            // Since we're about to handle this query now, remove it from
                            // includedQueriesById, in case it was added earlier because of
                            // options.include.
                            includedQueriesByOq.delete(oq);
                            let result = onQueryUpdated(oq, diff, lastDiff);
                            if (result === true) {
                                // The onQueryUpdated function requested the default refetching
                                // behavior by returning true.
                                result = oq
                                    .refetch()
                                    .retain( /* create a persistent subscription on the query */);
                            }
                            // Record the result in the results Map, as long as onQueryUpdated
                            // did not return false to skip/ignore this result.
                            if (result !== false) {
                                results.set(oq, result);
                            }
                            // Allow the default cache broadcast to happen, except when
                            // onQueryUpdated returns false.
                            return result;
                        }
                        if (onQueryUpdated !== null &&
                            oq.options.fetchPolicy !== "cache-only") {
                            // If we don't have an onQueryUpdated function, and onQueryUpdated
                            // was not disabled by passing null, make sure this query is
                            // "included" like any other options.include-specified query.
                            includedQueriesByOq.set(oq, { oq, lastDiff, diff });
                        }
                    }
                },
            });
        }
        if (includedQueriesByOq.size) {
            includedQueriesByOq.forEach(({ oq, lastDiff, diff }) => {
                let result;
                // If onQueryUpdated is provided, we want to use it for all included
                // queries, even the QueryOptions ones.
                if (onQueryUpdated) {
                    if (!diff) {
                        diff = oq.getCacheDiff();
                    }
                    result = onQueryUpdated(oq, diff, lastDiff);
                }
                // Otherwise, we fall back to refetching.
                if (!onQueryUpdated || result === true) {
                    result = oq
                        .refetch()
                        .retain( /* create a persistent subscription on the query */);
                }
                if (result !== false) {
                    results.set(oq, result);
                }
            });
        }
        if (removeOptimistic) {
            // In case no updateCache callback was provided (so cache.batch was not
            // called above, and thus did not already remove the optimistic layer),
            // remove it here. Since this is a no-op when the layer has already been
            // removed, we do it even if we called cache.batch above, since it's
            // possible this.cache is an instance of some ApolloCache subclass other
            // than InMemoryCache, and does not fully support the removeOptimistic
            // option for cache.batch.
            this.cache.removeOptimistic(removeOptimistic);
        }
        return results;
    }
    noCacheWarningsByCause = new WeakSet();
    maskOperation(options) {
        const { document, data } = options;
        if (__DEV__) {
            const { fetchPolicy, cause = {} } = options;
            const operationType = getOperationDefinition(document)?.operation;
            if (this.dataMasking &&
                fetchPolicy === "no-cache" &&
                !isFullyUnmaskedOperation(document) &&
                !this.noCacheWarningsByCause.has(cause)) {
                this.noCacheWarningsByCause.add(cause);
                __DEV__ && invariant.warn(101, getOperationName(document, `Unnamed ${operationType ?? "operation"}`));
            }
        }
        return (this.dataMasking ?
            maskOperation(data, document, this.cache)
            : data);
    }
    maskFragment(options) {
        const { data, fragment, fragmentName } = options;
        return this.dataMasking ?
            maskFragment(data, fragment, this.cache, fragmentName)
            : data;
    }
    fetchQueryByPolicy({ query, variables, fetchPolicy, errorPolicy, returnPartialData, context, networkStatus, }, { cacheWriteBehavior, onCacheHit, queryInfo, observableQuery, exposeExtensions, }) {
        const readCache = () => {
            return queryInfo.getDiff({
                query,
                variables,
                returnPartialData: true,
                optimistic: true,
            });
        };
        const resultsFromCache = (diff, networkStatus) => {
            const data = diff.result;
            if (__DEV__ && !returnPartialData && data !== null) {
                logMissingFieldErrors(diff.missing);
            }
            const toResult = (data, dataState = diff.dataState) => {
                // TODO: Eventually we should move this handling into
                // queryInfo.getDiff() directly. Since getDiff is updated to return null
                // on returnPartialData: false, we should take advantage of that instead
                // of having to patch it elsewhere.
                if (!diff.complete && !returnPartialData) {
                    data = undefined;
                    dataState = "empty";
                }
                return {
                    // TODO: Handle partial data
                    data: data,
                    dataState,
                    loading: isNetworkRequestInFlight(networkStatus),
                    networkStatus,
                    partial: !diff.complete,
                };
            };
            const fromData = (data) => {
                return of({
                    kind: "N",
                    value: toResult(data),
                    // Always attach the variables used for this fetch so @export
                    // resolution can update ObservableQuery.options.variables and
                    // resubscribe the cache watch under the correct variable set.
                    resolvedVariables: variables,
                    source: "cache",
                });
            };
            if (
            // Don't attempt to run forced resolvers if we have incomplete cache
            // data and partial isn't allowed since this result would get set to
            // `undefined` anyways in `toResult`.
            (diff.complete || returnPartialData) &&
                this.getDocumentInfo(query).hasForcedResolvers) {
                if (__DEV__) {
                    invariant(this.localState, 102, getOperationName(query, "(anonymous)"));
                }
                onCacheHit();
                return from(this.localState.execute({
                    client: this.client,
                    document: query,
                    remoteResult: data ? { data } : undefined,
                    context,
                    variables,
                    onlyRunForcedResolvers: true,
                    returnPartialData: true,
                    fetchPolicy,
                }).then((resolved) => ({
                    kind: "N",
                    value: toResult(resolved.data || void 0, diff.complete ? "complete"
                        : resolved.data ? "partial"
                            : "empty"),
                    // Always attach the variables used for this fetch so @export
                    // resolution can update ObservableQuery.options.variables and
                    // resubscribe the cache watch under the correct variable set.
                    resolvedVariables: variables,
                    source: "cache",
                })));
            }
            // Resolves https://github.com/apollographql/apollo-client/issues/10317.
            // If errorPolicy is 'none' and notifyOnNetworkStatusChange is true,
            // data was incorrectly returned from the cache on refetch:
            // if diff.missing exists, we should not return cache data.
            if (errorPolicy === "none" &&
                networkStatus === NetworkStatus.refetch &&
                diff.missing) {
                return fromData(void 0);
            }
            return fromData(data || undefined);
        };
        const resultsFromLink = ({ prunePendingDeferFragments = true, } = {}) => this.getResultsFromLink({
            query,
            variables,
            context,
            fetchPolicy,
            errorPolicy,
            returnPartialData,
            networkStatus,
        }, {
            cacheWriteBehavior,
            queryInfo,
            observableQuery,
            exposeExtensions,
            prunePendingDeferFragments,
        }).pipe(validateDidEmitValue(), materialize(), map((result) => ({
            ...result,
            // Always attach the variables used for this fetch so @export
            // resolution can update ObservableQuery.options.variables and
            // resubscribe the cache watch under the correct variable set.
            resolvedVariables: variables,
            source: "network",
        })));
        switch (fetchPolicy) {
            default:
            case "cache-first": {
                const diff = readCache();
                if (diff.complete) {
                    return {
                        fromLink: false,
                        observable: resultsFromCache(diff, NetworkStatus.ready),
                    };
                }
                if (returnPartialData) {
                    return {
                        fromLink: true,
                        observable: concat(resultsFromCache(diff, NetworkStatus.loading), resultsFromLink()),
                    };
                }
                return { fromLink: true, observable: resultsFromLink() };
            }
            case "cache-and-network": {
                const diff = readCache();
                if (diff.complete || returnPartialData) {
                    return {
                        fromLink: true,
                        observable: concat(resultsFromCache(diff, NetworkStatus.loading), resultsFromLink({ prunePendingDeferFragments: false })),
                    };
                }
                return { fromLink: true, observable: resultsFromLink() };
            }
            case "cache-only":
                return {
                    fromLink: false,
                    observable: concat(resultsFromCache(readCache(), NetworkStatus.ready)),
                };
            case "network-only":
                return {
                    fromLink: true,
                    observable: resultsFromLink({
                        prunePendingDeferFragments: networkStatus !== NetworkStatus.refetch,
                    }),
                };
            case "no-cache":
                return {
                    fromLink: true,
                    observable: resultsFromLink().pipe(map((notification) => {
                        if (notification.kind === "N" &&
                            notification.value.data != null &&
                            this.cache.configuresScalars()) {
                            notification.value.data = coerceScalarFieldsToParsed(notification.value.data, query, this.cache);
                        }
                        return notification;
                    })),
                };
            case "standby":
                return { fromLink: false, observable: EMPTY };
        }
    }
}
function validateDidEmitValue() {
    let didEmitValue = false;
    return tap({
        next() {
            didEmitValue = true;
        },
        complete() {
            invariant(didEmitValue, 103);
        },
    });
}
function isFullyUnmaskedOperation(document) {
    let isUnmasked = true;
    visit(document, {
        FragmentSpread: (node) => {
            isUnmasked =
                !!node.directives &&
                    node.directives.some((directive) => directive.name.value === "unmask");
            if (!isUnmasked) {
                return BREAK;
            }
        },
    });
    return isUnmasked;
}
function addNonReactiveToNamedFragments(document) {
    return visit(document, {
        FragmentSpread: (node) => {
            // Do not add `@nonreactive` if the fragment is marked with `@unmask`
            // since we want to react to changes in this fragment.
            if (node.directives?.some((directive) => directive.name.value === "unmask")) {
                return;
            }
            return {
                ...node,
                directives: [
                    ...(node.directives || []),
                    {
                        kind: Kind.DIRECTIVE,
                        name: { kind: Kind.NAME, value: "nonreactive" },
                    },
                ],
            };
        },
    });
}
function removeStreamDetailsFromExtensions(original) {
    if (original.extensions?.[streamInfoSymbol] == null) {
        return original;
    }
    const { extensions: { [streamInfoSymbol]: _, ...extensions }, ...result } = original;
    if (Object.keys(extensions).length > 0) {
        result.extensions = extensions;
    }
    return result;
}
//# sourceMappingURL=QueryManager.js.map