@apollo/client
Version:
A fully-featured caching GraphQL client.
1,102 lines (1,100 loc) • 59.9 kB
JavaScript
import { equal } from "@wry/equality";
import { BehaviorSubject, Observable, share, Subject, tap } from "rxjs";
import { isNetworkRequestInFlight } from "@apollo/client/utilities";
import { __DEV__ } from "@apollo/client/utilities/environment";
import { compact, equalByQuery, filterMap, getOperationDefinition, getOperationName, getQueryDefinition, preventUnhandledRejection, toQueryResult, } from "@apollo/client/utilities/internal";
import { invariant } from "@apollo/client/utilities/invariant";
import { NetworkStatus } from "./networkStatus.js";
const { assign, hasOwnProperty } = Object;
const uninitialized = {
loading: true,
networkStatus: NetworkStatus.loading,
data: undefined,
dataState: "empty",
partial: true,
};
const empty = {
loading: false,
networkStatus: NetworkStatus.ready,
data: undefined,
dataState: "empty",
partial: true,
};
export class ObservableQuery {
options;
queryName;
/**
* @internal will be read and written from `QueryInfo`
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
_lastWrite;
// The `query` computed property will always reflect the document transformed
// by the last run query. `this.options.query` will always reflect the raw
// untransformed query to ensure document transforms with runtime conditionals
// are run on the original document.
get query() {
return this.lastQuery;
}
/**
* An object containing the variables that were provided for the query.
*/
get variables() {
return this.options.variables;
}
unsubscribeFromCache;
input;
subject;
isTornDown;
queryManager;
subscriptions = new Set();
/**
* If an `ObservableQuery` is created with a `network-only` fetch policy,
* it should actually start receiving cache updates, but not before it has
* received the first result from the network.
*/
waitForNetworkResult;
lastQuery;
linkSubscription;
pollingInfo;
get networkStatus() {
return this.subject.getValue().result.networkStatus;
}
constructor({ queryManager, options, transformedQuery = queryManager.transform(options.query), }) {
this.queryManager = queryManager;
// active state
this.waitForNetworkResult = options.fetchPolicy === "network-only";
this.isTornDown = false;
this.subscribeToMore = this.subscribeToMore.bind(this);
this.maskResult = this.maskResult.bind(this);
const { watchQuery: { fetchPolicy: defaultFetchPolicy = "cache-first" } = {}, } = queryManager.defaultOptions;
const { fetchPolicy = defaultFetchPolicy,
// Make sure we don't store "standby" as the initialFetchPolicy.
initialFetchPolicy = fetchPolicy === "standby" ? defaultFetchPolicy : (fetchPolicy), } = options;
this.lastQuery = transformedQuery;
this.options = {
...options,
// Remember the initial options.fetchPolicy so we can revert back to this
// policy when variables change. This information can also be specified
// (or overridden) by providing options.initialFetchPolicy explicitly.
initialFetchPolicy,
// This ensures this.options.fetchPolicy always has a string value, in
// case options.fetchPolicy was not provided.
fetchPolicy,
variables: this.getVariablesWithDefaults(options.variables),
};
this.initializeObservablesQueue();
this["@@observable"] = () => this;
if (Symbol.observable) {
this[Symbol.observable] = () => this;
}
const opDef = getOperationDefinition(this.query);
this.queryName = opDef && opDef.name && opDef.name.value;
}
initializeObservablesQueue() {
this.subject = new BehaviorSubject({
query: this.query,
variables: this.variables,
result: uninitialized,
meta: {},
});
const observable = this.subject.pipe(tap({
subscribe: () => {
if (!this.subject.observed) {
this.reobserve();
// TODO: See if we can rework updatePolling to better handle this.
// reobserve calls updatePolling but this `subscribe` callback is
// called before the subject is subscribed to so `updatePolling`
// can't accurately detect if there is an active subscription.
// Calling it again here ensures that it can detect if it can poll
setTimeout(() => this.updatePolling());
}
},
unsubscribe: () => {
if (!this.subject.observed) {
this.tearDownQuery();
}
},
}), filterMap(({ query, variables, result: current, meta }, context) => {
const { shouldEmit } = meta;
if (current === uninitialized) {
// reset internal state after `ObservableQuery.reset()`
context.previous = undefined;
context.previousVariables = undefined;
}
if (this.options.fetchPolicy === "standby" ||
shouldEmit === 2 /* EmitBehavior.never */)
return;
if (shouldEmit === 1 /* EmitBehavior.force */)
return emit();
const { previous, previousVariables } = context;
if (previous) {
const documentInfo = this.queryManager.getDocumentInfo(query);
const dataMasking = this.queryManager.dataMasking;
const maskedQuery = dataMasking ? documentInfo.nonReactiveQuery : query;
const resultIsEqual = dataMasking || documentInfo.hasNonreactiveDirective ?
equalByQuery(maskedQuery, previous, current, variables)
: equal(previous, current);
if (resultIsEqual && equal(previousVariables, variables)) {
return;
}
}
if (shouldEmit === 3 /* EmitBehavior.networkStatusChange */ &&
(!this.options.notifyOnNetworkStatusChange ||
equal(previous, current))) {
return;
}
return emit();
function emit() {
context.previous = current;
context.previousVariables = variables;
return current;
}
}, () => ({})));
this.pipe = observable.pipe.bind(observable);
this.subscribe = observable.subscribe.bind(observable);
this.input = new Subject();
// we want to feed many streams into `this.subject`, but none of them should
// be able to close `this.input`
this.input.complete = () => { };
this.input.pipe(this.operator).subscribe(this.subject);
}
// We can't use Observable['subscribe'] here as the type as it conflicts with
// the ability to infer T from Subscribable<T>. This limits the surface area
// to the non-deprecated signature which works properly with type inference.
/**
* Subscribes to the `ObservableQuery`.
* @param observerOrNext - Either an RxJS `Observer` with some or all callback methods,
* or the `next` handler that is called for each value emitted from the subscribed Observable.
* @returns A subscription reference to the registered handlers.
*/
subscribe;
/**
* Used to stitch together functional operators into a chain.
*
* @example
*
* ```ts
* import { filter, map } from 'rxjs';
*
* observableQuery
* .pipe(
* filter(...),
* map(...),
* )
* .subscribe(x => console.log(x));
* ```
*
* @returns The Observable result of all the operators having been called
* in the order they were passed in.
*/
pipe;
[Symbol.observable];
["@@observable"];
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
getCacheDiff({ optimistic = true } = {}) {
return this.queryManager.cache.diff({
query: this.query,
variables: this.variables,
returnPartialData: true,
optimistic,
});
}
getInitialResult(initialFetchPolicy) {
const fetchPolicy = this.queryManager.prioritizeCacheValues ?
"cache-first"
: initialFetchPolicy || this.options.fetchPolicy;
const cacheResult = () => {
const diff = this.getCacheDiff();
// TODO: queryInfo.getDiff should handle this since cache.diff returns a
// null when returnPartialData is false
const data = this.options.returnPartialData || diff.complete ?
diff.result ?? undefined
: undefined;
return this.maskResult({
data,
dataState: diff.complete ? "complete"
: data === undefined ? "empty"
: "partial",
loading: !diff.complete,
networkStatus: diff.complete ? NetworkStatus.ready : NetworkStatus.loading,
partial: !diff.complete,
});
};
switch (fetchPolicy) {
case "cache-only": {
return {
...cacheResult(),
loading: false,
networkStatus: NetworkStatus.ready,
};
}
case "cache-first":
return cacheResult();
case "cache-and-network":
return {
...cacheResult(),
loading: true,
networkStatus: NetworkStatus.loading,
};
case "standby":
return empty;
default:
return uninitialized;
}
}
resubscribeCache() {
const { variables, fetchPolicy } = this.options;
const query = this.query;
const shouldUnsubscribe = fetchPolicy === "standby" ||
fetchPolicy === "no-cache" ||
this.waitForNetworkResult;
const shouldResubscribe = !isEqualQuery({ query, variables }, this.unsubscribeFromCache) &&
!this.waitForNetworkResult;
if (shouldUnsubscribe || shouldResubscribe) {
this.unsubscribeFromCache?.();
}
if (shouldUnsubscribe || !shouldResubscribe) {
return;
}
const watch = {
query,
variables,
optimistic: true,
watcher: this,
callback: (diff) => {
const info = this.queryManager.getDocumentInfo(query);
if (info.hasClientExports || info.hasForcedResolvers) {
// If this is not set to something different than `diff`, we will
// not be notified about future cache changes with an equal `diff`.
// That would be the case if we are working with client-only fields
// that are forced or with `exports` fields that might change, causing
// local resovlers to return a new result.
// This is based on an implementation detail of `InMemoryCache`, which
// is not optimal - but the only alternative to this would be to
// resubscribe to the cache asynchonouly, which would bear the risk of
// missing further synchronous updates.
watch.lastDiff = undefined;
}
if (watch.lastOwnDiff === diff) {
// skip cache updates that were caused by our own writes
return;
}
const { result: previousResult } = this.subject.getValue();
if (!diff.complete &&
// If we are trying to deliver an incomplete cache result, we avoid
// reporting it if the query has errored, otherwise we let the broadcast try
// and repair the partial result by refetching the query. This check avoids
// a situation where a query that errors and another succeeds with
// overlapping data does not report the partial data result to the errored
// query.
//
// See https://github.com/apollographql/apollo-client/issues/11400 for more
// information on this issue.
(previousResult.error ||
// Prevent to schedule a notify directly after the `ObservableQuery`
// has been `reset` (which will set the `previousResult` to `uninitialized` or `empty`)
// as in those cases, `resetCache` will manually call `refetch` with more intentional timing.
previousResult === uninitialized ||
previousResult === empty)) {
return;
}
if (!equal(previousResult.data, diff.result)) {
this.scheduleNotify();
}
},
};
const cancelWatch = this.queryManager.cache.watch(watch);
this.unsubscribeFromCache = Object.assign(() => {
this.unsubscribeFromCache = undefined;
cancelWatch();
}, { query, variables });
}
stableLastResult;
getCurrentResult() {
const { result: current } = this.subject.getValue();
let value = (
// if the `current` result is in an error state, we will always return that
// error state, even if we have no observers
(current.networkStatus === NetworkStatus.error ||
// if we have observers, we are watching the cache and
// this.subject.getValue() will always be up to date
this.hasObservers() || // if we are using a `no-cache` fetch policy in which case this
// `ObservableQuery` cannot have been updated from the outside - in
// that case, we prefer to keep the current value
this.options.fetchPolicy === "no-cache")) ?
current
// otherwise, the `current` value might be outdated due to missed
// external updates - calculate it again
: this.getInitialResult();
if (value === uninitialized) {
value = this.getInitialResult();
}
if (!equal(this.stableLastResult, value)) {
this.stableLastResult = value;
}
return this.stableLastResult;
}
/**
* Update the variables of this observable query, and fetch the new results.
* This method should be preferred over `setVariables` in most use cases.
*
* Returns a `ResultPromise` with an additional `.retain()` method. Calling
* `.retain()` keeps the network operation running even if the `ObservableQuery`
* no longer requires the result.
*
* Note: `refetch()` guarantees that a value will be emitted from the
* observable, even if the result is deep equal to the previous value.
*
* @param variables - The new set of variables. If there are missing variables,
* the previous values of those variables will be used.
*/
refetch(variables) {
const { fetchPolicy } = this.options;
const reobserveOptions = {
// Always disable polling for refetches.
pollInterval: 0,
};
// Unless the provided fetchPolicy always consults the network
// (no-cache, network-only, or cache-and-network), override it with
// network-only to force the refetch for this fetchQuery call.
if (fetchPolicy === "no-cache") {
reobserveOptions.fetchPolicy = "no-cache";
}
else {
reobserveOptions.fetchPolicy = "network-only";
}
if (__DEV__ && variables && hasOwnProperty.call(variables, "variables")) {
const queryDef = getQueryDefinition(this.query);
const vars = queryDef.variableDefinitions;
if (!vars || !vars.some((v) => v.variable.name.value === "variables")) {
__DEV__ && invariant.warn(77, variables, queryDef.name?.value || queryDef);
}
}
if (variables && !equal(this.variables, variables)) {
// Update the existing options with new variables
reobserveOptions.variables = this.options.variables =
this.getVariablesWithDefaults({ ...this.variables, ...variables });
}
this._lastWrite = undefined;
return this._reobserve(reobserveOptions, {
newNetworkStatus: NetworkStatus.refetch,
});
}
fetchMore({ query, variables, context, errorPolicy, updateQuery, }) {
invariant(
this.options.fetchPolicy !== "cache-only",
78,
getOperationName(this.query, "(anonymous)")
);
const combinedOptions = {
...compact(this.options, { errorPolicy: "none" }, {
query,
context,
errorPolicy,
}),
variables: (query ? variables : ({
...this.variables,
...variables,
})),
// The fetchMore request goes immediately to the network and does
// not automatically write its result to the cache (hence no-cache
// instead of network-only), because we allow the caller of
// fetchMore to provide an updateQuery callback that determines how
// the data gets written to the cache.
fetchPolicy: "no-cache",
notifyOnNetworkStatusChange: this.options.notifyOnNetworkStatusChange,
};
combinedOptions.query = this.transformDocument(combinedOptions.query);
// If a temporary query is passed to `fetchMore`, we don't want to store
// it as the last query result since it may be an optimized query for
// pagination. We will however run the transforms on the original document
// as well as the document passed in `fetchMoreOptions` to ensure the cache
// uses the most up-to-date document which may rely on runtime conditionals.
this.lastQuery =
query ?
this.transformDocument(this.options.query)
: combinedOptions.query;
let wasUpdated = false;
const isCached = this.options.fetchPolicy !== "no-cache";
if (!isCached) {
invariant(updateQuery, 79);
}
const { finalize, pushNotification } = this.pushOperation(NetworkStatus.fetchMore);
pushNotification({
source: "newNetworkStatus",
kind: "N",
value: {},
}, { shouldEmit: 3 /* EmitBehavior.networkStatusChange */ });
return this.queryManager
.fetchQuery(combinedOptions, NetworkStatus.fetchMore)
.then((fetchMoreResult) => {
// disable the `fetchMore` override that is currently active
// the next updates caused by this should not be `fetchMore` anymore,
// but `ready` or whatever other calculated loading state is currently
// appropriate
finalize();
if (isCached) {
// Performing this cache update inside a cache.batch transaction ensures
// any affected cache.watch watchers are notified at most once about any
// updates. Most watchers will be using the QueryInfo class, which
// responds to notifications by calling reobserveCacheFirst to deliver
// fetchMore cache results back to this ObservableQuery.
this.queryManager.cache.batch({
update: (cache) => {
if (updateQuery) {
cache.updateQuery({
query: this.query,
variables: this.variables,
returnPartialData: true,
optimistic: false,
}, (previous) => updateQuery(previous, {
fetchMoreResult: fetchMoreResult.data,
variables: combinedOptions.variables,
}));
}
else {
// If we're using a field policy instead of updateQuery, the only
// thing we need to do is write the new data to the cache using
// combinedOptions.variables (instead of this.variables, which is
// what this.updateQuery uses, because it works by abusing the
// original field value, keyed by the original variables).
cache.writeQuery({
query: combinedOptions.query,
variables: combinedOptions.variables,
data: fetchMoreResult.data,
});
}
},
onWatchUpdated: (watch) => {
if (watch.watcher === this) {
wasUpdated = true;
}
},
});
}
else {
// There is a possibility `lastResult` may not be set when
// `fetchMore` is called which would cause this to crash. This should
// only happen if we haven't previously reported a result. We don't
// quite know what the right behavior should be here since this block
// of code runs after the fetch result has executed on the network.
// We plan to let it crash in the meantime.
//
// If we get bug reports due to the `data` property access on
// undefined, this should give us a real-world scenario that we can
// use to test against and determine the right behavior. If we do end
// up changing this behavior, this may require, for example, an
// adjustment to the types on `updateQuery` since that function
// expects that the first argument always contains previous result
// data, but not `undefined`.
const lastResult = this.getCurrentResult();
const data = updateQuery(lastResult.data, {
fetchMoreResult: fetchMoreResult.data,
variables: combinedOptions.variables,
});
// was reportResult
pushNotification({
kind: "N",
value: {
...lastResult,
networkStatus: NetworkStatus.ready,
// will be overwritten anyways, just here for types sake
loading: false,
data: data,
dataState: lastResult.dataState === "streaming" ? "streaming" : "complete",
},
source: "network",
});
}
return this.maskResult(fetchMoreResult);
})
.finally(() => {
// call `finalize` a second time in case the `.then` case above was not reached
finalize();
// In case the cache writes above did not generate a broadcast
// notification (which would have been intercepted by onWatchUpdated),
// likely because the written data were the same as what was already in
// the cache, we still want fetchMore to deliver its final loading:false
// result with the unchanged data.
if (isCached && !wasUpdated) {
pushNotification({
kind: "N",
source: "newNetworkStatus",
value: {},
}, { shouldEmit: 1 /* EmitBehavior.force */ });
}
});
}
// XXX the subscription variables are separate from the query variables.
// if you want to update subscription variables, right now you have to do that separately,
// and you can only do it by stopping the subscription and then subscribing again with new variables.
/**
* A function that enables you to execute a [subscription](https://www.apollographql.com/docs/react/data/subscriptions/), usually to subscribe to specific fields that were included in the query.
*
* This function returns _another_ function that you can call to terminate the subscription.
*/
subscribeToMore(options) {
const subscription = this.queryManager
.startGraphQLSubscription({
query: options.document,
variables: options.variables,
context: options.context,
})
.subscribe({
next: (subscriptionData) => {
const { updateQuery, onError } = options;
const { error } = subscriptionData;
if (error) {
if (onError) {
onError(error);
}
else {
invariant.error(80, error);
}
return;
}
if (updateQuery) {
this.updateQuery((previous, updateOptions) => updateQuery(previous, {
subscriptionData: subscriptionData,
...updateOptions,
}));
}
},
});
this.subscriptions.add(subscription);
return () => {
if (this.subscriptions.delete(subscription)) {
subscription.unsubscribe();
}
};
}
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
applyOptions(newOptions) {
const mergedOptions = compact(this.options, newOptions || {});
assign(this.options, mergedOptions);
this.updatePolling();
}
/**
* Update the variables of this observable query, and fetch the new results
* if they've changed. Most users should prefer `refetch` instead of
* `setVariables` in order to to be properly notified of results even when
* they come from the cache.
*
* Note: `setVariables()` guarantees that a value will be emitted from the
* observable, even if the result is deeply equal to the previous value.
*
* Note: the promise will resolve with the last emitted result
* when either the variables match the current variables or there
* are no subscribers to the query.
*
* @param variables - The new set of variables. If there are missing variables,
* the previous values of those variables will be used.
*/
async setVariables(variables) {
variables = this.getVariablesWithDefaults(variables);
if (equal(this.variables, variables)) {
// If we have no observers, then we don't actually want to make a network
// request. As soon as someone observes the query, the request will kick
// off. For now, we just store any changes. (See #1077)
return toQueryResult(this.getCurrentResult());
}
this.options.variables = variables;
// See comment above
if (!this.hasObservers()) {
return toQueryResult(this.getCurrentResult());
}
return this._reobserve({
// Reset options.fetchPolicy to its original value.
fetchPolicy: this.options.initialFetchPolicy,
variables,
}, { newNetworkStatus: NetworkStatus.setVariables });
}
/**
* A function that enables you to update the query's cached result without executing a followup GraphQL operation.
*
* See [using updateQuery and updateFragment](https://www.apollographql.com/docs/react/caching/cache-interaction/#using-updatequery-and-updatefragment) for additional information.
*/
updateQuery(mapFn) {
const { queryManager } = this;
const { result, complete } = this.getCacheDiff({ optimistic: false });
const newResult = mapFn(result, {
variables: this.variables,
complete: !!complete,
previousData: result,
});
if (newResult) {
queryManager.cache.writeQuery({
query: this.options.query,
data: newResult,
variables: this.variables,
});
queryManager.broadcastQueries();
}
}
/**
* A function that instructs the query to begin re-executing at a specified interval (in milliseconds).
*/
startPolling(pollInterval) {
this.options.pollInterval = pollInterval;
this.updatePolling();
}
/**
* A function that instructs the query to stop polling after a previous call to `startPolling`.
*/
stopPolling() {
this.options.pollInterval = 0;
this.updatePolling();
}
// Update options.fetchPolicy according to options.nextFetchPolicy.
applyNextFetchPolicy(reason,
// It's possible to use this method to apply options.nextFetchPolicy to
// options.fetchPolicy even if options !== this.options, though that happens
// most often when the options are temporary, used for only one request and
// then thrown away, so nextFetchPolicy may not end up mattering.
options) {
if (options.nextFetchPolicy) {
const { fetchPolicy = "cache-first", initialFetchPolicy = fetchPolicy } = options;
if (fetchPolicy === "standby") {
// Do nothing, leaving options.fetchPolicy unchanged.
}
else if (typeof options.nextFetchPolicy === "function") {
// When someone chooses "cache-and-network" or "network-only" as their
// initial FetchPolicy, they often do not want future cache updates to
// trigger unconditional network requests, which is what repeatedly
// applying the "cache-and-network" or "network-only" policies would
// seem to imply. Instead, when the cache reports an update after the
// initial network request, it may be desirable for subsequent network
// requests to be triggered only if the cache result is incomplete. To
// that end, the options.nextFetchPolicy option provides an easy way to
// update options.fetchPolicy after the initial network request, without
// having to call observableQuery.reobserve.
options.fetchPolicy = options.nextFetchPolicy.call(options, fetchPolicy, { reason, options, observable: this, initialFetchPolicy });
}
else if (reason === "variables-changed") {
options.fetchPolicy = initialFetchPolicy;
}
else {
options.fetchPolicy = options.nextFetchPolicy;
}
}
return options.fetchPolicy;
}
fetch(options, networkStatus, fetchQuery, operator) {
// TODO Make sure we update the networkStatus (and infer fetchVariables)
// before actually committing to the fetch.
const initialFetchPolicy = this.options.fetchPolicy;
options.context ??= {};
let synchronouslyEmitted = false;
const onCacheHit = () => {
synchronouslyEmitted = true;
};
const fetchQueryOperator = // we cannot use `tap` here, since it allows only for a "before subscription"
// hook with `subscribe` and we care for "directly before and after subscription"
(source) => new Observable((subscriber) => {
try {
return source.subscribe({
next(value) {
synchronouslyEmitted = true;
subscriber.next(value);
},
error: (error) => subscriber.error(error),
complete: () => subscriber.complete(),
});
}
finally {
if (!synchronouslyEmitted) {
operation.override = networkStatus;
this.input.next({
kind: "N",
source: "newNetworkStatus",
value: {
resetError: true,
},
query,
variables,
meta: {
shouldEmit: 3 /* EmitBehavior.networkStatusChange */,
/*
* The moment this notification is emitted, `nextFetchPolicy`
* might already have switched from a `network-only` to a
* `cache-something` policy, so we want to ensure that the
* loading state emit doesn't accidentally read from the cache
* in those cases.
*/
fetchPolicy: initialFetchPolicy,
},
});
}
}
});
let { observable, fromLink } = this.queryManager.fetchObservableWithInfo(options, {
networkStatus,
query: fetchQuery,
onCacheHit,
fetchQueryOperator,
observableQuery: this,
});
// track query and variables from the start of the operation
const { query, variables } = this;
const operation = {
abort: () => {
subscription.unsubscribe();
},
query,
variables,
};
this.activeOperations.add(operation);
let forceFirstValueEmit = networkStatus == NetworkStatus.refetch ||
networkStatus == NetworkStatus.setVariables;
observable = observable.pipe(operator, share());
const subscription = observable
.pipe(tap({
next: (notification) => {
if (notification.source === "newNetworkStatus" ||
(notification.kind === "N" && notification.value.loading)) {
operation.override = networkStatus;
}
else {
delete operation.override;
}
},
finalize: () => this.activeOperations.delete(operation),
}))
.subscribe({
next: (value) => {
const meta = {};
if (forceFirstValueEmit &&
value.kind === "N" &&
"loading" in value.value &&
!value.value.loading) {
forceFirstValueEmit = false;
meta.shouldEmit = 1 /* EmitBehavior.force */;
}
this.input.next({ ...value, query, variables, meta });
},
});
return { fromLink, subscription, observable };
}
// Turns polling on or off based on this.options.pollInterval.
didWarnCacheOnlyPolling = false;
updatePolling() {
// Avoid polling in SSR mode
if (this.queryManager.ssrMode) {
return;
}
const { pollingInfo, options: { fetchPolicy, pollInterval }, } = this;
if (!pollInterval || !this.hasObservers() || fetchPolicy === "cache-only") {
if (__DEV__) {
if (!this.didWarnCacheOnlyPolling &&
pollInterval &&
fetchPolicy === "cache-only") {
__DEV__ && invariant.warn(81, getOperationName(this.query, "(anonymous)"));
this.didWarnCacheOnlyPolling = true;
}
}
this.cancelPolling();
return;
}
if (pollingInfo?.interval === pollInterval) {
return;
}
const info = pollingInfo || (this.pollingInfo = {});
info.interval = pollInterval;
const maybeFetch = () => {
if (this.pollingInfo) {
if (!isNetworkRequestInFlight(this.networkStatus) &&
!this.options.skipPollAttempt?.()) {
this._reobserve({
// Most fetchPolicy options don't make sense to use in a polling context, as
// users wouldn't want to be polling the cache directly. However, network-only and
// no-cache are both useful for when the user wants to control whether or not the
// polled results are written to the cache.
fetchPolicy: this.options.initialFetchPolicy === "no-cache" ?
"no-cache"
: "network-only",
}, {
newNetworkStatus: NetworkStatus.poll,
}).then(poll, poll);
}
else {
poll();
}
}
};
const poll = () => {
const info = this.pollingInfo;
if (info) {
clearTimeout(info.timeout);
info.timeout = setTimeout(maybeFetch, info.interval);
}
};
poll();
}
// This differs from stopPolling in that it does not set pollInterval to 0
cancelPolling() {
if (this.pollingInfo) {
clearTimeout(this.pollingInfo.timeout);
delete this.pollingInfo;
}
}
/**
* Reevaluate the query, optionally against new options. New options will be
* merged with the current options when given.
*
* Note: `variables` can be reset back to their defaults (typically empty) by calling `reobserve` with
* `variables: undefined`.
*/
reobserve(newOptions) {
return this._reobserve(newOptions);
}
_reobserve(newOptions, internalOptions) {
this.isTornDown = false;
let { newNetworkStatus } = internalOptions || {};
this.queryManager.obsQueries.add(this);
const useDisposableObservable =
// Refetching uses a disposable Observable to allow refetches using different
// options, without permanently altering the options of the
// original ObservableQuery.
newNetworkStatus === NetworkStatus.refetch ||
// Polling uses a disposable Observable so the polling options (which force
// fetchPolicy to be "network-only" or "no-cache") won't override the original options.
newNetworkStatus === NetworkStatus.poll;
// Save the old variables, since Object.assign may modify them below.
const oldVariables = this.variables;
const oldFetchPolicy = this.options.fetchPolicy;
const mergedOptions = compact(this.options, newOptions || {});
const options = useDisposableObservable ?
// Disposable Observable fetches receive a shallow copy of this.options
// (merged with newOptions), leaving this.options unmodified.
mergedOptions
: assign(this.options, mergedOptions);
// Don't update options.query with the transformed query to avoid
// overwriting this.options.query when we aren't using a disposable concast.
// We want to ensure we can re-run the custom document transforms the next
// time a request is made against the original query.
const query = this.transformDocument(options.query);
this.lastQuery = query;
// Reevaluate variables to allow resetting variables with variables: undefined,
// otherwise `compact` will ignore the `variables` key in `newOptions`. We
// do this after we run the query transform to ensure we get default
// variables from the transformed query.
//
// Note: updating options.variables may mutate this.options.variables
// in the case of a non-disposable query. This is intentional.
if (newOptions && "variables" in newOptions) {
options.variables = this.getVariablesWithDefaults(newOptions.variables);
}
if (!useDisposableObservable) {
// We can skip calling updatePolling if we're not changing this.options.
this.updatePolling();
// Reset options.fetchPolicy to its original value when variables change,
// unless a new fetchPolicy was provided by newOptions.
if (newOptions &&
newOptions.variables &&
!equal(newOptions.variables, oldVariables) &&
// Don't mess with the fetchPolicy if it's currently "standby".
options.fetchPolicy !== "standby" &&
// If we're changing the fetchPolicy anyway, don't try to change it here
// using applyNextFetchPolicy. The explicit options.fetchPolicy wins.
(options.fetchPolicy === oldFetchPolicy ||
// A `nextFetchPolicy` function has even higher priority, though,
// so in that case `applyNextFetchPolicy` must be called.
typeof options.nextFetchPolicy === "function")) {
// This might mutate options.fetchPolicy
this.applyNextFetchPolicy("variables-changed", options);
if (newNetworkStatus === void 0) {
newNetworkStatus = NetworkStatus.setVariables;
}
}
}
const oldNetworkStatus = this.networkStatus;
if (!newNetworkStatus) {
newNetworkStatus = NetworkStatus.loading;
if (oldNetworkStatus !== NetworkStatus.loading &&
newOptions?.variables &&
!equal(newOptions.variables, oldVariables)) {
newNetworkStatus = NetworkStatus.setVariables;
}
// QueryManager does not emit any values for standby fetch policies so we
// want ensure that the networkStatus remains ready.
if (options.fetchPolicy === "standby") {
newNetworkStatus = NetworkStatus.ready;
}
}
if (options.fetchPolicy === "standby") {
this.cancelPolling();
}
this.resubscribeCache();
const { promise, operator: promiseOperator } = getTrackingOperatorPromise((value) => {
switch (value.kind) {
case "E":
throw value.error;
case "N":
if (value.source !== "newNetworkStatus" && !value.value.loading)
return value.value;
}
},
// This default value should only be used when using a `fetchPolicy` of
// `standby` since that fetch policy completes without emitting a
// result. Since we are converting this to a QueryResult type, we
// omit the extra fields from ApolloQueryResult in the default value.
options.fetchPolicy === "standby" ?
{ data: undefined }
: undefined);
const { subscription, observable, fromLink } = this.fetch(options, newNetworkStatus, query, promiseOperator);
if (!useDisposableObservable && (fromLink || !this.linkSubscription)) {
if (this.linkSubscription) {
this.linkSubscription.unsubscribe();
}
this.linkSubscription = subscription;
}
const ret = Object.assign(preventUnhandledRejection(promise
.then((result) => toQueryResult(this.maskResult(result)))
.finally(() => {
if (!this.hasObservers() && this.activeOperations.size === 0) {
// If `reobserve` was called on a query without any obervers,
// the teardown logic would never be called, so we need to
// call it here to ensure the query is properly torn down.
this.tearDownQuery();
}
})), {
retain: () => {
const subscription = observable.subscribe({});
const unsubscribe = () => subscription.unsubscribe();
promise.then(unsubscribe, unsubscribe);
return ret;
},
});
return ret;
}
hasObservers() {
return this.subject.observed;
}
/**
* Tears down the `ObservableQuery` and stops all active operations by sending a `complete` notification.
*/
stop() {
this.subject.complete();
this.initializeObservablesQueue();
this.tearDownQuery();
}
tearDownQuery() {
if (this.isTornDown)
return;
this.resetNotifications();
this.unsubscribeFromCache?.();
if (this.linkSubscription) {
this.linkSubscription.unsubscribe();
delete this.linkSubscription;
}
this.stopPolling();
// stop all active GraphQL subscriptions
this.subscriptions.forEach((sub) => sub.unsubscribe());
this.subscriptions.clear();
this.queryManager.obsQueries.delete(this);
this.isTornDown = true;
this.abortActiveOperations();
this._lastWrite = undefined;
}
transformDocument(document) {
return this.queryManager.transform(document);
}
maskResult(result) {
const masked = this.queryManager.maskOperation({
document: this.query,
data: result.data,
fetchPolicy: this.options.fetchPolicy,
cause: this,
});
// Maintain object identity as much as possible
return masked === result.data ? result : { ...result, data: masked };
}
dirty = false;
notifyTimeout;
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
resetNotifications() {
if (this.notifyTimeout) {
clearTimeout(this.notifyTimeout);
this.notifyTimeout = void 0;
}
this.dirty = false;
}
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
scheduleNotify() {
if (this.dirty)
return;
this.dirty = true;
if (!this.notifyTimeout) {
this.notifyTimeout = setTimeout(() => this.notify(true), 0);
}
}
/**
* @internal
*
* @deprecated This is an internal API and should not be used directly. This can be removed or changed at any time.
*/
notify(scheduled = false) {
if (!scheduled) {
// For queries with client exports or forced resolvers, we don't want to
// synchronously reobserve the cache on broadcast,
// but actually wait for the `scheduleNotify` timeout triggered by the
// `cache.watch` callback from `resubscribeCache`.
const info = this.queryManager.getDocumentInfo(this.query);
if (info.hasClientExports || info.hasForcedResolvers) {
return;
}
}
const { dirty } = this;
this.resetNotifications();
if (dirty &&
(this.options.fetchPolicy == "cache-only" ||
this.options.fetchPolicy == "cache-and-network" ||
!this.activeOperations.size)) {
const diff = this.getCacheDiff();
if (
// `fromOptimisticTransaction` is not avaiable through the `cache.diff`
// code path, so we need to check it this way
equal(diff.result, this.getCacheDiff({ optimistic: false }).result)) {
//If this diff did not come from an optimistic transaction
// make the ObservableQuery "reobserve" the latest data
// using a temporary fetch policy of "cache-first", so complete cache
// results have a chance to be delivered without triggering additional
// network requests, even when options.fetchPolicy is "network-only"
// or "cache-and-network". All other fetch policies are preserved by
// this method, and are handled by calling oq.reobserve(). If this
// reobservation is spurious, distinctUntilChanged still has a
// chance to catch it before delivery to ObservableQuery subscribers.
this.reobserveCacheFirst();
}
else {
// If this diff came from an optimistic transaction, deliver the
// current cache data to the ObservableQuery, but don't perform a
// reobservation, since oq.reobserveCacheFirst might make a network
// request, and we never want to trigger network requests in the
// middle of optimistic updates.
this.input.next({
kind: "N",
value: {
data: diff.result,
dataState: diff.complete ? "complete"
: diff.result ? "partial"
: "empty",
networkStatus: NetworkStatus.ready,
loading: false,