import { Trie } from "@wry/trie";
import { OperationTypeNode } from "graphql";
import { catchError, concat, EMPTY, filter, from, lastValueFrom, map, mergeMap, mergeWith, Observable, of, share, shareReplay, Subject, tap, } from "rxjs";
import { canonicalStringify } from "@apollo/client/cache";
import { CombinedGraphQLErrors, graphQLResultHasProtocolErrors, toErrorLike, } from "@apollo/client/errors";
import { PROTOCOL_ERRORS_SYMBOL } from "@apollo/client/errors";
import { execute } from "@apollo/client/link/core";
import { maskFragment, maskOperation } from "@apollo/client/masking";
import { checkDocument, print } from "@apollo/client/utilities";
import { AutoCleanedWeakCache, cacheSizes } from "@apollo/client/utilities";
import { addNonReactiveToNamedFragments, hasDirectives, isExecutionPatchIncrementalResult, isExecutionPatchResult, isFullyUnmaskedOperation, removeDirectivesFromDocument, } from "@apollo/client/utilities";
import { DocumentTransform, getDefaultValues, getGraphQLErrorsFromResult, getOperationDefinition, getOperationName, graphQLResultHasError, hasClientExports, isDocumentNode, isNonEmptyArray, isNonNullObject, makeUniqueId, } from "@apollo/client/utilities";
import { mergeIncrementalData } from "@apollo/client/utilities";
import { __DEV__ } from "@apollo/client/utilities/environment";
import { onAnyEvent, toQueryResult } from "@apollo/client/utilities/internal";
import { invariant, newInvariantError, } from "@apollo/client/utilities/invariant";
import { isNetworkRequestInFlight, NetworkStatus } from "./networkStatus.js";
import { logMissingFieldErrors, ObservableQuery } from "./ObservableQuery.js";
import { QueryInfo, shouldWriteResult, } from "./QueryInfo.js";
const { hasOwnProperty } = Object.prototype;
const IGNORE = {};
export class QueryManager {
    cache;
    link;
    defaultOptions;
    assumeImmutableResults;
    documentTransform;
    ssrMode;
    defaultContext;
    dataMasking;
    queryDeduplication;
    clientAwareness = {};
    localState;
    /**
     * 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 the queries that the QueryManager is currently managing (not
    // including mutations and subscriptions).
    queries = new Map();
    // Maps from queryId 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.cache = options.cache;
        this.link = options.link;
        this.defaultOptions = options.defaultOptions;
        this.queryDeduplication = options.queryDeduplication;
        this.clientAwareness = options.clientAwareness;
        this.localState = options.localState;
        this.ssrMode = options.ssrMode;
        this.assumeImmutableResults = options.assumeImmutableResults;
        this.dataMasking = options.dataMasking;
        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;
        this.defaultContext = options.defaultContext || {};
        if ((this.onBroadcast = options.onBroadcast)) {
            this.mutationStore = {};
        }
    }
    /**
     * Call this method to terminate any active query processes, making it safe
     * to dispose of this QueryManager instance.
     */
    stop() {
        this.queries.forEach((_info, queryId) => {
            this.removeQuery(queryId);
        });
        this.cancelPendingFetches(newInvariantError(72));
    }
    cancelPendingFetches(error) {
        this.fetchCancelFns.forEach((cancel) => cancel(error));
        this.fetchCancelFns.clear();
    }
    async mutate({ mutation, variables, optimisticResponse, updateQueries, refetchQueries = [], awaitRefetchQueries = false, update: updateWithProxyFn, onQueryUpdated, fetchPolicy = this.defaultOptions.mutate?.fetchPolicy || "network-only", errorPolicy = this.defaultOptions.mutate?.errorPolicy || "none", keepRootFields, context, }) {
        invariant(mutation, 73);
        checkDocument(mutation, OperationTypeNode.MUTATION);
        invariant(fetchPolicy === "network-only" || fetchPolicy === "no-cache", 74);
        const mutationId = this.generateMutationId();
        mutation = this.cache.transformForLink(this.transform(mutation));
        const { hasClientExports } = this.getDocumentInfo(mutation);
        variables = this.getVariables(mutation, variables);
        if (hasClientExports) {
            variables = (await this.localState.addExportedVariables(mutation, variables, context));
        }
        const mutationStoreValue = this.mutationStore &&
            (this.mutationStore[mutationId] = {
                mutation,
                variables,
                loading: true,
                error: null,
            });
        const isOptimistic = optimisticResponse &&
            this.markMutationOptimistic(optimisticResponse, {
                mutationId,
                document: mutation,
                variables,
                fetchPolicy,
                errorPolicy,
                context,
                updateQueries,
                update: updateWithProxyFn,
                keepRootFields,
            });
        this.broadcastQueries();
        return new Promise((resolve, reject) => {
            return this.getObservableFromLink(mutation, {
                ...context,
                optimisticResponse: isOptimistic ? optimisticResponse : void 0,
            }, variables, {}, false)
                .pipe(validateDidEmitValue(), mergeMap((result) => {
                const hasErrors = graphQLResultHasError(result);
                if (hasErrors && errorPolicy === "none") {
                    throw new CombinedGraphQLErrors(result);
                }
                if (mutationStoreValue) {
                    mutationStoreValue.loading = false;
                    mutationStoreValue.error = null;
                }
                const storeResult = { ...result };
                if (typeof refetchQueries === "function") {
                    refetchQueries = refetchQueries(storeResult);
                }
                if (errorPolicy === "ignore" && hasErrors) {
                    delete storeResult.errors;
                }
                return from(this.markMutationResult({
                    mutationId,
                    result: storeResult,
                    document: mutation,
                    variables,
                    fetchPolicy,
                    errorPolicy,
                    context,
                    update: updateWithProxyFn,
                    updateQueries,
                    awaitRefetchQueries,
                    refetchQueries,
                    removeOptimistic: isOptimistic ? mutationId : void 0,
                    onQueryUpdated,
                    keepRootFields,
                }));
            }))
                .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 (!("hasNext" in storeResult) || storeResult.hasNext === false) {
                        const result = {
                            data: this.maskOperation({
                                document: mutation,
                                data: storeResult.data,
                                fetchPolicy,
                                id: mutationId,
                            }),
                        };
                        if (graphQLResultHasError(storeResult)) {
                            result.error = new CombinedGraphQLErrors(storeResult);
                        }
                        if (storeResult.extensions) {
                            result.extensions = storeResult.extensions;
                        }
                        resolve(result);
                    }
                },
                error: (err) => {
                    const error = toErrorLike(err);
                    if (mutationStoreValue) {
                        mutationStoreValue.loading = false;
                        mutationStoreValue.error = error;
                    }
                    if (isOptimistic) {
                        this.cache.removeOptimistic(mutationId);
                    }
                    this.broadcastQueries();
                    if (errorPolicy === "ignore") {
                        return resolve({ data: undefined });
                    }
                    if (errorPolicy === "all") {
                        return resolve({ data: undefined, error });
                    }
                    reject(error);
                },
            });
        });
    }
    markMutationResult(mutation, cache = this.cache) {
        let { result } = mutation;
        const cacheWrites = [];
        const skipCache = mutation.fetchPolicy === "no-cache";
        if (!skipCache && shouldWriteResult(result, mutation.errorPolicy)) {
            if (!isExecutionPatchIncrementalResult(result)) {
                cacheWrites.push({
                    result: result.data,
                    dataId: "ROOT_MUTATION",
                    query: mutation.document,
                    variables: mutation.variables,
                });
            }
            if (isExecutionPatchIncrementalResult(result) &&
                isNonEmptyArray(result.incremental)) {
                const diff = cache.diff({
                    id: "ROOT_MUTATION",
                    // The cache complains if passed a mutation where it expects a
                    // query, so we transform mutations and subscriptions to queries
                    // (only once, thanks to this.transformCache).
                    query: this.getDocumentInfo(mutation.document).asQuery,
                    variables: mutation.variables,
                    optimistic: false,
                    returnPartialData: true,
                });
                let mergedData;
                if (diff.result) {
                    mergedData = mergeIncrementalData(diff.result, result);
                }
                if (typeof mergedData !== "undefined") {
                    // cast the ExecutionPatchResult to FetchResult here since
                    // ExecutionPatchResult never has `data` when returned from the server
                    result.data = mergedData;
                    cacheWrites.push({
                        result: mergedData,
                        dataId: "ROOT_MUTATION",
                        query: mutation.document,
                        variables: mutation.variables,
                    });
                }
            }
            const { updateQueries } = mutation;
            if (updateQueries) {
                this.queries.forEach(({ observableQuery }, queryId) => {
                    const queryName = observableQuery && observableQuery.queryName;
                    if (!queryName || !hasOwnProperty.call(updateQueries, queryName)) {
                        return;
                    }
                    const updater = updateQueries[queryName];
                    const { document, variables } = this.queries.get(queryId);
                    // Read the current query result from the store.
                    const { result: currentQueryResult, complete } = cache.diff({
                        query: document,
                        variables,
                        returnPartialData: true,
                        optimistic: false,
                    });
                    if (complete && currentQueryResult) {
                        // Run our reducer using the current query result and the mutation result.
                        const nextQueryResult = updater(currentQueryResult, {
                            mutationResult: result,
                            queryName: (document && getOperationName(document)) || void 0,
                            queryVariables: variables,
                        });
                        // Write the modified result back into the store if we got a new result.
                        if (nextQueryResult) {
                            cacheWrites.push({
                                result: nextQueryResult,
                                dataId: "ROOT_QUERY",
                                query: document,
                                variables,
                            });
                        }
                    }
                });
            }
        }
        if (cacheWrites.length > 0 ||
            (mutation.refetchQueries || "").length > 0 ||
            mutation.update ||
            mutation.onQueryUpdated ||
            mutation.removeOptimistic) {
            const results = [];
            this.refetchQueries({
                updateCache: (cache) => {
                    if (!skipCache) {
                        cacheWrites.forEach((write) => cache.write(write));
                    }
                    // If the mutation has some writes associated with it then we need to
                    // apply those writes to the store by running this reducer again with
                    // a write action.
                    const { update } = mutation;
                    // Determine whether result is a SingleExecutionResult,
                    // or the final ExecutionPatchResult.
                    const isFinalResult = !isExecutionPatchResult(result) ||
                        (isExecutionPatchIncrementalResult(result) && !result.hasNext);
                    if (update) {
                        if (!skipCache) {
                            // Re-read the ROOT_MUTATION data we just wrote into the cache
                            // (the first cache.write call in the cacheWrites.forEach loop
                            // above), so field read functions have a chance to run for
                            // fields within mutation result objects.
                            const diff = cache.diff({
                                id: "ROOT_MUTATION",
                                // The cache complains if passed a mutation where it expects a
                                // query, so we transform mutations and subscriptions to queries
                                // (only once, thanks to this.transformCache).
                                query: this.getDocumentInfo(mutation.document).asQuery,
                                variables: mutation.variables,
                                optimistic: false,
                                returnPartialData: true,
                            });
                            if (diff.complete) {
                                result = { ...result, data: diff.result };
                                if ("incremental" in result) {
                                    delete result.incremental;
                                }
                                if ("hasNext" in result) {
                                    delete result.hasNext;
                                }
                            }
                        }
                        // If we've received the whole response,
                        // either a SingleExecutionResult or the final ExecutionPatchResult,
                        // call the update function.
                        if (isFinalResult) {
                            update(cache, result, {
                                context: mutation.context,
                                variables: mutation.variables,
                            });
                        }
                    }
                    // TODO Do this with cache.evict({ id: 'ROOT_MUTATION' }) but make it
                    // shallow to allow rolling back optimistic evictions.
                    if (!skipCache && !mutation.keepRootFields && isFinalResult) {
                        cache.modify({
                            id: "ROOT_MUTATION",
                            fields(value, { fieldName, DELETE }) {
                                return fieldName === "__typename" ? value : DELETE;
                            },
                        });
                    }
                },
                include: mutation.refetchQueries,
                // Write the final mutation.result to the root layer of the cache.
                optimistic: false,
                // Remove the corresponding optimistic layer at the same time as we
                // write the final non-optimistic result.
                removeOptimistic: mutation.removeOptimistic,
                // Let the caller of client.mutate optionally determine the refetching
                // behavior for watched queries after the mutation.update function runs.
                // If no onQueryUpdated function was provided for this mutation, pass
                // null instead of undefined to disable the default refetching behavior.
                onQueryUpdated: mutation.onQueryUpdated || null,
            }).forEach((result) => results.push(result));
            if (mutation.awaitRefetchQueries || mutation.onQueryUpdated) {
                // Returning a promise here makes the mutation await that promise, so we
                // include results in that promise's work if awaitRefetchQueries or an
                // onQueryUpdated function was specified.
                return Promise.all(results).then(() => result);
            }
        }
        return Promise.resolve(result);
    }
    markMutationOptimistic(optimisticResponse, mutation) {
        const data = typeof optimisticResponse === "function" ?
            optimisticResponse(mutation.variables, { IGNORE })
            : optimisticResponse;
        if (data === IGNORE) {
            return false;
        }
        this.cache.recordOptimisticTransaction((cache) => {
            try {
                this.markMutationResult({
                    ...mutation,
                    result: { data },
                }, cache);
            }
            catch (error) {
                __DEV__ && invariant.error(error);
            }
        }, mutation.mutationId);
        return true;
    }
    fetchQuery(queryId, options, networkStatus) {
        return lastValueFrom(this.fetchObservableWithInfo(this.getOrCreateQuery(queryId), options, networkStatus).observable.pipe(map(toQueryResult)), {
            // 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 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: hasClientExports(document),
                hasForcedResolvers: this.localState.shouldForceResolvers(document),
                hasNonreactiveDirective: hasDirectives(["nonreactive"], document),
                nonReactiveQuery: addNonReactiveToNamedFragments(document),
                clientQuery: this.localState.clientQuery(document),
                serverQuery: removeDirectivesFromDocument([
                    { name: "client", remove: true },
                    { name: "connection" },
                    { name: "nonreactive" },
                    { name: "unmask" },
                ], document),
                defaultVars: getDefaultValues(getOperationDefinition(document)),
                // 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);
        }
        return transformCache.get(document);
    }
    getVariables(document, variables) {
        return {
            ...this.getDocumentInfo(document).defaultVars,
            ...variables,
        };
    }
    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 queryInfo = new QueryInfo(this);
        const observable = new ObservableQuery({
            queryManager: this,
            queryInfo,
            options,
        });
        observable["lastQuery"] = query;
        if (!ObservableQuery["inactiveOnCreation"].getValue()) {
            this.queries.set(observable.queryId, queryInfo);
        }
        // We give queryInfo the transformed query to ensure the first cache diff
        // uses the transformed query instead of the raw query
        queryInfo.init({ document: query, variables: observable.variables });
        queryInfo.setObservableQuery(observable);
        return observable;
    }
    query(options, queryId = this.generateQueryId()) {
        checkDocument(options.query, OperationTypeNode.QUERY);
        const query = this.transform(options.query);
        return this.fetchQuery(queryId, { ...options, query })
            .then((value) => ({
            ...value,
            data: this.maskOperation({
                document: query,
                data: value?.data,
                fetchPolicy: options.fetchPolicy,
                id: queryId,
            }),
        }))
            .finally(() => this.stopQuery(queryId));
    }
    queryIdCounter = 1;
    generateQueryId() {
        return String(this.queryIdCounter++);
    }
    requestIdCounter = 1;
    generateRequestId() {
        return this.requestIdCounter++;
    }
    mutationIdCounter = 1;
    generateMutationId() {
        return String(this.mutationIdCounter++);
    }
    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(75));
        this.queries.forEach((queryInfo) => {
            if (queryInfo.observableQuery) {
                // Set loading to true so listeners don't trigger unless they want
                // results with partial data.
                queryInfo.observableQuery["networkStatus"] = NetworkStatus.loading;
            }
            else {
                queryInfo.stop();
            }
        });
        if (this.mutationStore) {
            this.mutationStore = {};
        }
        // begin removing data from the store
        return this.cache.reset(options);
    }
    getObservableQueries(include = "active") {
        const queries = new Map();
        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.queries.forEach(({ observableQuery: oq, document }, queryId) => {
            if (oq) {
                if (include === "all") {
                    queries.set(queryId, oq);
                    return;
                }
                const { queryName, options: { fetchPolicy }, } = oq;
                if (fetchPolicy === "standby" ||
                    (include === "active" && !oq.hasObservers())) {
                    return;
                }
                if (include === "active" ||
                    (queryName && queryNamesAndQueryStrings.has(queryName)) ||
                    (document && queryNamesAndQueryStrings.has(print(document)))) {
                    queries.set(queryId, oq);
                    if (queryName)
                        queryNamesAndQueryStrings.set(queryName, true);
                    if (document)
                        queryNamesAndQueryStrings.set(print(document), true);
                }
            }
        });
        if (legacyQueryOptions.size) {
            legacyQueryOptions.forEach((options) => {
                // We will be issuing a fresh network request for this query, so we
                // pre-allocate a new query ID here, using a special prefix to enable
                // cleaning up these temporary queries later, after fetching.
                const queryId = makeUniqueId("legacyOneTimeQuery");
                const queryInfo = this.getOrCreateQuery(queryId).init({
                    document: options.query,
                    variables: options.variables,
                });
                const oq = new ObservableQuery({
                    queryManager: this,
                    queryInfo,
                    options: {
                        ...options,
                        fetchPolicy: "network-only",
                    },
                });
                invariant(oq.queryId === queryId);
                queryInfo.setObservableQuery(oq);
                queries.set(queryId, oq);
            });
        }
        if (__DEV__ && queryNamesAndQueryStrings.size) {
            queryNamesAndQueryStrings.forEach((included, nameOrQueryString) => {
                if (!included) {
                    const queryName = queryNames.get(nameOrQueryString);
                    if (queryName) {
                        __DEV__ && invariant.warn(76, queryName);
                    }
                    else {
                        __DEV__ && invariant.warn(77);
                    }
                }
            });
        }
        return queries;
    }
    reFetchObservableQueries(includeStandby = false) {
        const observableQueryPromises = [];
        this.getObservableQueries(includeStandby ? "all" : "active").forEach((observableQuery, queryId) => {
            const { fetchPolicy } = observableQuery.options;
            observableQuery.resetLastResults();
            if (includeStandby ||
                (fetchPolicy !== "standby" && fetchPolicy !== "cache-only")) {
                observableQueryPromises.push(observableQuery.refetch());
            }
            (this.queries.get(queryId) || observableQuery["queryInfo"]).setDiff(null);
        });
        this.broadcastQueries();
        return Promise.all(observableQueryPromises);
    }
    startGraphQLSubscription(options) {
        let { query, variables } = options;
        const { fetchPolicy, errorPolicy = "none", context = {}, extensions = {}, } = options;
        checkDocument(query, OperationTypeNode.SUBSCRIPTION);
        query = this.transform(query);
        variables = this.getVariables(query, variables);
        const makeObservable = (variables) => this.getObservableFromLink(query, context, variables, extensions).pipe(map((rawResult) => {
            if (fetchPolicy !== "no-cache") {
                // the subscription interface should handle not sending us results we no longer subscribe to.
                // XXX I don't think we ever send in an object with errors, but we might in the future...
                if (shouldWriteResult(rawResult, errorPolicy)) {
                    this.cache.write({
                        query,
                        result: rawResult.data,
                        dataId: "ROOT_SUBSCRIPTION",
                        variables: variables,
                    });
                }
                this.broadcastQueries();
            }
            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: toErrorLike(error) });
        }), filter((result) => !!(result.data || result.error)));
        if (this.getDocumentInfo(query).hasClientExports) {
            const observablePromise = this.localState
                .addExportedVariables(query, variables, context)
                .then(makeObservable);
            return new Observable((observer) => {
                let sub = null;
                observablePromise.then((observable) => (sub = observable.subscribe(observer)), observer.error);
                return () => sub && sub.unsubscribe();
            });
        }
        return makeObservable(variables);
    }
    stopQuery(queryId) {
        this.removeQuery(queryId);
        this.broadcastQueries();
    }
    removeQuery(queryId) {
        // teardown all links
        // Both `QueryManager.fetchRequest` and `QueryManager.query` create separate promises
        // that each add their reject functions to fetchCancelFns.
        // A query created with `QueryManager.query()` could trigger a `QueryManager.fetchRequest`.
        // The same queryId could have two rejection fns for two promises
        this.fetchCancelFns.delete(queryId);
        if (this.queries.has(queryId)) {
            this.queries.get(queryId)?.stop();
            this.queries.delete(queryId);
        }
    }
    broadcastQueries() {
        if (this.onBroadcast)
            this.onBroadcast();
        this.queries.forEach((info) => info.observableQuery?.["notify"]());
    }
    getLocalState() {
        return this.localState;
    }
    // Use protected instead of private field so
    // @apollo/experimental-nextjs-app-support can access type info.
    inFlightLinkObservables = new Trie(false);
    getObservableFromLink(query, context, variables, extensions, 
    // Prefer context.queryDeduplication if specified.
    deduplication = context?.queryDeduplication ??
        this.queryDeduplication) {
        let observable;
        const { serverQuery, clientQuery } = this.getDocumentInfo(query);
        if (serverQuery) {
            const { inFlightLinkObservables, link } = this;
            const operation = {
                query: serverQuery,
                variables,
                operationName: getOperationName(serverQuery) || void 0,
                context: this.prepareContext({
                    ...context,
                    forceFetch: !deduplication,
                }),
                extensions,
            };
            context = operation.context;
            if (deduplication) {
                const printedServerQuery = print(serverQuery);
                const varJson = canonicalStringify(variables);
                const entry = inFlightLinkObservables.lookup(printedServerQuery, varJson);
                observable = entry.observable;
                if (!observable) {
                    observable = entry.observable = execute(link, operation).pipe(onAnyEvent((event) => {
                        if ((event.type !== "next" ||
                            !("hasNext" in event.value) ||
                            !event.value.hasNext) &&
                            inFlightLinkObservables.peek(printedServerQuery, varJson) ===
                                entry) {
                            inFlightLinkObservables.remove(printedServerQuery, varJson);
                        }
                    }), shareReplay({ refCount: true }));
                }
            }
            else {
                observable = execute(link, operation);
            }
        }
        else {
            observable = of({ data: {} });
            context = this.prepareContext(context);
        }
        if (clientQuery) {
            observable = observable.pipe(mergeMap((result) => {
                return from(this.localState.runResolvers({
                    document: clientQuery,
                    remoteResult: result,
                    context,
                    variables,
                }));
            }));
        }
        return observable;
    }
    getResultsFromLink(queryInfo, cacheWriteBehavior, options) {
        const requestId = (queryInfo.lastRequestId = this.generateRequestId());
        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).pipe(map((result) => {
            const graphQLErrors = getGraphQLErrorsFromResult(result);
            const hasErrors = graphQLErrors.length > 0;
            // If we interrupted this request by calling getResultsFromLink again
            // with the same QueryInfo object, we ignore the old results.
            if (requestId >= queryInfo.lastRequestId) {
                if (hasErrors && errorPolicy === "none") {
                    queryInfo.resetLastWrite();
                    queryInfo.observableQuery?.["resetNotifications"]();
                    // Throwing here effectively calls observer.error.
                    throw new CombinedGraphQLErrors(result);
                }
                // Use linkDocument rather than queryInfo.document so the
                // operation/fragments used to write the result are the same as the
                // ones used to obtain it from the link.
                queryInfo.markResult(result, linkDocument, options, cacheWriteBehavior);
            }
            const aqr = {
                data: result.data,
                loading: false,
                networkStatus: NetworkStatus.ready,
                partial: !result.data,
            };
            // In the case we start multiple network requests simulatenously, we
            // want to ensure we properly set `data` if we're reporting on an old
            // result which will not be caught by the conditional above that ends up
            // throwing the markError result.
            if (hasErrors && errorPolicy === "none") {
                aqr.data = void 0;
            }
            if (hasErrors && errorPolicy !== "ignore") {
                aqr.error = new CombinedGraphQLErrors(result);
                aqr.networkStatus = NetworkStatus.error;
            }
            return aqr;
        }), catchError((error) => {
            error = toErrorLike(error);
            // Avoid storing errors from older interrupted queries.
            if (requestId >= queryInfo.lastRequestId && errorPolicy === "none") {
                queryInfo.resetLastWrite();
                queryInfo.observableQuery?.["resetNotifications"]();
                throw error;
            }
            const aqr = {
                data: undefined,
                loading: false,
                networkStatus: NetworkStatus.ready,
                partial: true,
            };
            if (errorPolicy !== "ignore") {
                aqr.error = error;
                aqr.networkStatus = NetworkStatus.error;
            }
            return of(aqr);
        }));
    }
    fetchObservableWithInfo(queryInfo, 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, emitLoadingState = false) {
        const variables = this.getVariables(query, options.variables);
        const defaults = this.defaultOptions.watchQuery;
        let { fetchPolicy = (defaults && defaults.fetchPolicy) || "cache-first", errorPolicy = (defaults && defaults.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,
            notifyOnNetworkStatusChange,
            context,
        });
        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 observableWithInfo = this.fetchQueryByPolicy(queryInfo, normalized, networkStatus, emitLoadingState);
            if (
            // If we're in standby, postpone advancing options.fetchPolicy using
            // applyNextFetchPolicy.
            normalized.fetchPolicy !== "standby" &&
                queryInfo.observableQuery) {
                queryInfo.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.queryId);
            // We need to call `complete` on the subject here otherwise the merged
            // observable will never complete since it waits for all source
            // observables to complete before itself completes.
            fetchCancelSubject.complete();
        };
        this.fetchCancelFns.set(queryInfo.queryId, (reason) => {
            fetchCancelSubject.error(reason);
            cleanupCancelFn();
        });
        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) {
            observable = from(this.localState.addExportedVariables(normalized.query, normalized.variables, 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 {
            observable: observable.pipe(tap({ error: cleanupCancelFn, complete: cleanupCancelFn }), mergeWith(fetchCancelSubject), share()),
            fromLink: containsDataFromLink,
        };
    }
    refetchQueries({ updateCache, include, optimistic = false, removeOptimistic = optimistic ? makeUniqueId("refetchQueries") : void 0, onQueryUpdated, }) {
        const includedQueriesById = new Map();
        if (include) {
            this.getObservableQueries(include).forEach((oq, queryId) => {
                includedQueriesById.set(queryId, {
                    oq,
                    lastDiff: (this.queries.get(queryId) || oq["queryInfo"]).getDiff(),
                });
            });
        }
        const results = new Map();
        if (updateCache) {
            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 instanceof QueryInfo && watch.watcher.observableQuery;
                    if (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.
                            includedQueriesById.delete(oq.queryId);
                            let result = onQueryUpdated(oq, diff, lastDiff);
                            if (result === true) {
                                // The onQueryUpdated function requested the default refetching
                                // behavior by returning true.
                                result = oq.refetch();
                            }
                            // 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) {
                            // 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.
                            includedQueriesById.set(oq.queryId, { oq, lastDiff, diff });
                        }
                    }
                },
            });
        }
        if (includedQueriesById.size) {
            includedQueriesById.forEach(({ oq, lastDiff, diff }, queryId) => {
                let result;
                // If onQueryUpdated is provided, we want to use it for all included
                // queries, even the QueryOptions ones.
                if (onQueryUpdated) {
                    if (!diff) {
                        diff = this.cache.diff(oq["queryInfo"]["getDiffOptions"]());
                    }
                    result = onQueryUpdated(oq, diff, lastDiff);
                }
                // Otherwise, we fall back to refetching.
                if (!onQueryUpdated || result === true) {
                    result = oq.refetch();
                }
                if (result !== false) {
                    results.set(oq, result);
                }
                if (queryId.indexOf("legacyOneTimeQuery") >= 0) {
                    this.removeQuery(queryId);
                }
            });
        }
        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;
    }
    noCacheWarningsByQueryId = new Set();
    maskOperation(options) {
        const { document, data } = options;
        if (__DEV__) {
            const { fetchPolicy, id } = options;
            const operationType = getOperationDefinition(document)?.operation;
            const operationId = (operationType?.[0] ?? "o") + id;
            if (this.dataMasking &&
                fetchPolicy === "no-cache" &&
                !isFullyUnmaskedOperation(document) &&
                !this.noCacheWarningsByQueryId.has(operationId)) {
                this.noCacheWarningsByQueryId.add(operationId);
                __DEV__ && invariant.warn(78, 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(queryInfo, { query, variables, fetchPolicy, refetchWritePolicy, errorPolicy, returnPartialData, context, }, 
    // The initial networkStatus for this fetch, most often
    // NetworkStatus.loading, but also possibly fetchMore, poll, refetch,
    // or setVariables.
    newNetworkStatus, emitLoadingState) {
        queryInfo.init({
            document: query,
            variables,
        });
        const readCache = () => queryInfo.getDiff();
        const resultsFromCache = (diff, networkStatus = newNetworkStatus) => {
            const data = diff.result;
            if (__DEV__ && !returnPartialData && data !== null) {
                logMissingFieldErrors(diff.missing);
            }
            const toResult = (data) => {
                // 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;
                }
                return {
                    // TODO: Handle partial data
                    data: data,
                    loading: isNetworkRequestInFlight(networkStatus),
                    networkStatus,
                    partial: !diff.complete,
                };
            };
            const fromData = (data) => {
                return of(toResult(data));
            };
            if (this.getDocumentInfo(query).hasForcedResolvers) {
                return from(this.localState
                    .runResolvers({
                    document: query,
                    // TODO: Update remoteResult to handle `null`. In v3 the `if`
                    // statement contained a check against `data`, but this value was
                    // always `{}` if nothing was in the cache, which meant the check
                    // above always succeeded when there were forced resolvers. Now that
                    // `data` is nullable, this `remoteResult` needs to be an empty
                    // object. Ideally we can pass in `null` here and the resolvers
                    // would be able to handle this the same way.
                    remoteResult: { data: data || {} },
                    context,
                    variables,
                    onlyRunForcedResolvers: true,
                })
                    .then((resolved) => toResult(resolved.data || void 0)));
            }
            // 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 cacheWriteBehavior = fetchPolicy === "no-cache" ? 0 /* CacheWriteBehavior.FORBID */
            // Watched queries must opt into overwriting existing data on refetch,
            // by passing refetchWritePolicy: "overwrite" in their WatchQueryOptions.
            : (newNetworkStatus === NetworkStatus.refetch &&
                refetchWritePolicy !== "merge") ?
                1 /* CacheWriteBehavior.OVERWRITE */
                : 2 /* CacheWriteBehavior.MERGE */;
        const resultsFromLink = () => this.getResultsFromLink(queryInfo, cacheWriteBehavior, {
            query,
            variables,
            context,
            fetchPolicy,
            errorPolicy,
        }).pipe(validateDidEmitValue());
        switch (fetchPolicy) {
            default:
            case "cache-first": {
                const diff = readCache();
                if (diff.complete) {
                    return {
                        fromLink: false,
                        observable: resultsFromCache(diff, NetworkStatus.ready),
                    };
                }
                if (returnPartialData || emitLoadingState) {
                    return {
                        fromLink: true,
                        observable: concat(resultsFromCache(diff), resultsFromLink()),
                    };
                }
                return { fromLink: true, observable: resultsFromLink() };
            }
            case "cache-and-network": {
                const diff = readCache();
                if (diff.complete || returnPartialData || emitLoadingState) {
                    return {
                        fromLink: true,
                        observable: concat(resultsFromCache(diff), resultsFromLink()),
                    };
                }
                return { fromLink: true, observable: resultsFromLink() };
            }
            case "cache-only":
                return {
                    fromLink: false,
                    observable: concat(resultsFromCache(readCache(), NetworkStatus.ready)),
                };
            case "network-only":
                if (emitLoadingState) {
                    return {
                        fromLink: true,
                        observable: concat(resultsFromCache(readCache()), resultsFromLink()),
                    };
                }
                return { fromLink: true, observable: resultsFromLink() };
            case "no-cache":
                if (emitLoadingState) {
                    return {
                        fromLink: true,
                        // Note that queryInfo.getDiff() for no-cache queries does not call
                        // cache.diff, but instead returns a { complete: false } stub result
                        // when there is no queryInfo.diff already defined.
                        observable: concat(resultsFromCache(queryInfo.getDiff()), resultsFromLink()),
                    };
                }
                return { fromLink: true, observable: resultsFromLink() };
            case "standby":
                return { fromLink: false, observable: EMPTY };
        }
    }
    getOrCreateQuery(queryId) {
        if (queryId && !this.queries.has(queryId)) {
            this.queries.set(queryId, new QueryInfo(this, queryId));
        }
        return this.queries.get(queryId);
    }
    prepareContext(context = {}) {
        const newContext = this.localState.prepareContext(context);
        return {
            ...this.defaultContext,
            ...newContext,
            clientAwareness: this.clientAwareness,
        };
    }
}
function validateDidEmitValue() {
    let didEmitValue = false;
    return tap({
        next() {
            didEmitValue = true;
        },
        complete() {
            invariant(didEmitValue, 79);
        },
    });
}
//# sourceMappingURL=QueryManager.js.map
