UNPKG

@apollo/client

Version:

A fully-featured caching GraphQL client.

1,086 lines 51.3 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, filterMap, getDefaultValues, getOperationDefinition, getOperationName, graphQLResultHasError, hasDirectives, hasForcedResolvers, isDocumentNode, isNonNullObject, makeUniqueId, removeDirectivesFromDocument, toQueryResult, } from "@apollo/client/utilities/internal"; import { invariant, newInvariantError, } from "@apollo/client/utilities/invariant"; 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; 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(84)); } 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, 85, 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, 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, {}, 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(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, }), })); } requestIdCounter = 1; generateRequestId() { return this.requestIdCounter++; } 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(86)); 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: { ...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(87, queryName); } else { __DEV__ && invariant.warn(88); } } }); } 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, 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, 89, 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, 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, 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, }; 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, 90, operation[0].toUpperCase() + operation.slice(1), operationName ?? "(anonymous)" ); } invariant( !hasIncrementalDirective, 91, 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, })); })); } return { restart: () => entry.restart?.(), observable: entry.observable.pipe(catchError((error) => { error = toErrorLike(error); registerLinkError(error); throw error; })), }; } getResultsFromLink(options, { queryInfo, cacheWriteBehavior, observableQuery, }) { 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).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 result = queryInfo.markQueryResult(incoming, { ...options, document: linkDocument, cacheWriteBehavior, }); const hasErrors = graphQLResultHasError(result); if (hasErrors && errorPolicy === "none") { queryInfo.resetLastWrite(); observableQuery?.["resetNotifications"](); throw new CombinedGraphQLErrors(result); } const aqr = { data: result.data, ...(queryInfo.hasNext ? { loading: true, networkStatus: NetworkStatus.streaming, dataState: "streaming", partial: true, } : { dataState: result.data ? "complete" : "empty", loading: false, networkStatus: NetworkStatus.ready, partial: !result.data, }), }; // In the case we start multiple network requests simultaneously, 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) { if (errorPolicy === "none") { aqr.data = void 0; aqr.dataState = "empty"; } if (errorPolicy !== "ignore") { aqr.error = new CombinedGraphQLErrors(result); if (aqr.dataState !== "streaming") { aqr.networkStatus = NetworkStatus.error; } } } return aqr; }), catchError((error) => { // Avoid storing errors from older interrupted queries. if (requestId >= queryInfo.lastRequestId && errorPolicy === "none") { queryInfo.resetLastWrite(); 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, }) { 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 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 }); 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, 92, 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(93, 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, }, { cacheWriteBehavior, onCacheHit, queryInfo, observableQuery, }) { const readCache = () => this.cache.diff({ 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) => { // 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, dataState: diff.complete ? "complete" : data ? "partial" : "empty", loading: isNetworkRequestInFlight(networkStatus), networkStatus, partial: !diff.complete, }; }; const fromData = (data) => { return of({ kind: "N", value: toResult(data), 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, 94, getOperationName(query, "(anonymous)")); } onCacheHit(); return from(this.localState.execute({ client: this.client, document: query, remoteResult: data ? { data } : undefined, context, variables, onlyRunForcedResolvers: true, returnPartialData: true, }).then((resolved) => ({ kind: "N", value: toResult(resolved.data || void 0), 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 = () => this.getResultsFromLink({ query, variables, context, fetchPolicy, errorPolicy, }, { cacheWriteBehavior, queryInfo, observableQuery, }).pipe(validateDidEmitValue(), materialize(), map((result) => ({ ...result, 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()), }; } 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() }; case "no-cache": return { fromLink: true, observable: resultsFromLink() }; case "standby": return { fromLink: false, observable: EMPTY }; } } } function validateDidEmitValue() { let didEmitValue = false; return tap({ next() { didEmitValue = true; }, complete