@apollo/client
Version:
A fully-featured caching GraphQL client.
1,086 lines • 51.3 kB
JavaScript
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