@apollo/client
Version:
A fully-featured caching GraphQL client.
552 lines (551 loc) • 23.1 kB
JavaScript
import { OperationTypeNode } from "graphql";
import { map } from "rxjs";
import { NotImplementedHandler } from "@apollo/client/incremental";
import { execute } from "@apollo/client/link";
import { DocumentTransform } from "@apollo/client/utilities";
import { __DEV__ } from "@apollo/client/utilities/environment";
import { checkDocument, compact, getApolloClientMemoryInternals, mergeOptions, removeMaskedFragmentSpreads, } from "@apollo/client/utilities/internal";
import { invariant } from "@apollo/client/utilities/invariant";
import { version } from "../version.js";
import { QueryManager } from "./QueryManager.js";
let hasSuggestedDevtools = false;
/**
* This is the primary Apollo Client class. It is used to send GraphQL documents (i.e. queries
* and mutations) to a GraphQL spec-compliant server over an `ApolloLink` instance,
* receive results from the server and cache the results in a store. It also delivers updates
* to GraphQL queries through `Observable` instances.
*/
export class ApolloClient {
link;
cache;
/**
* @deprecated `disableNetworkFetches` has been renamed to `prioritizeCacheValues`.
*/
disableNetworkFetches;
set prioritizeCacheValues(value) {
this.queryManager.prioritizeCacheValues = value;
}
/**
* Whether to prioritize cache values over network results when `query` or `watchQuery` 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 created `ObservableQuery` long-term.
*
* This can e.g. be used to prioritize the cache during the first render after SSR.
*/
get prioritizeCacheValues() {
return this.queryManager.prioritizeCacheValues;
}
version;
queryDeduplication;
defaultOptions;
devtoolsConfig;
queryManager;
devToolsHookCb;
resetStoreCallbacks = [];
clearStoreCallbacks = [];
/**
* Constructs an instance of `ApolloClient`.
*
* @example
*
* ```js
* import { ApolloClient, InMemoryCache } from "@apollo/client";
*
* const cache = new InMemoryCache();
*
* const client = new ApolloClient({
* // Provide required constructor fields
* cache: cache,
* uri: "http://localhost:4000/",
*
* // Provide some optional constructor fields
* name: "react-web-client",
* version: "1.3",
* queryDeduplication: false,
* defaultOptions: {
* watchQuery: {
* fetchPolicy: "cache-and-network",
* },
* },
* });
* ```
*/
constructor(options) {
if (__DEV__) {
invariant(options.cache, 65);
invariant(options.link, 66);
}
const { cache, documentTransform, ssrMode = false, ssrForceFetchDelay = 0, queryDeduplication = true, defaultOptions, defaultContext, assumeImmutableResults = cache.assumeImmutableResults, localState, devtools, dataMasking, link, incrementalHandler = new NotImplementedHandler(), } = options;
this.link = link;
this.cache = cache;
this.queryDeduplication = queryDeduplication;
this.defaultOptions = defaultOptions || {};
this.devtoolsConfig = {
...devtools,
enabled: devtools?.enabled ?? __DEV__,
};
this.watchQuery = this.watchQuery.bind(this);
this.query = this.query.bind(this);
this.mutate = this.mutate.bind(this);
this.watchFragment = this.watchFragment.bind(this);
this.resetStore = this.resetStore.bind(this);
this.reFetchObservableQueries = this.refetchObservableQueries =
this.refetchObservableQueries.bind(this);
this.version = version;
this.queryManager = new QueryManager({
client: this,
defaultOptions: this.defaultOptions,
defaultContext,
documentTransform,
queryDeduplication,
ssrMode,
dataMasking: !!dataMasking,
clientOptions: options,
incrementalHandler,
assumeImmutableResults,
onBroadcast: this.devtoolsConfig.enabled ?
() => {
if (this.devToolsHookCb) {
this.devToolsHookCb();
}
}
: void 0,
localState,
});
this.prioritizeCacheValues = ssrMode || ssrForceFetchDelay > 0;
if (ssrForceFetchDelay) {
setTimeout(() => {
this.prioritizeCacheValues = false;
}, ssrForceFetchDelay);
}
if (this.devtoolsConfig.enabled)
this.connectToDevTools();
}
connectToDevTools() {
if (typeof window === "undefined") {
return;
}
const windowWithDevTools = window;
const devtoolsSymbol = Symbol.for("apollo.devtools");
(windowWithDevTools[devtoolsSymbol] =
windowWithDevTools[devtoolsSymbol] || []).push(this);
windowWithDevTools.__APOLLO_CLIENT__ = this;
/**
* Suggest installing the devtools for developers who don't have them
*/
if (!hasSuggestedDevtools && __DEV__) {
hasSuggestedDevtools = true;
if (window.document &&
window.top === window.self &&
/^(https?|file):$/.test(window.location.protocol)) {
setTimeout(() => {
if (!window.__APOLLO_DEVTOOLS_GLOBAL_HOOK__) {
const nav = window.navigator;
const ua = nav && nav.userAgent;
let url;
if (typeof ua === "string") {
if (ua.indexOf("Chrome/") > -1) {
url =
"https://chrome.google.com/webstore/detail/" +
"apollo-client-developer-t/jdkknkkbebbapilgoeccciglkfbmbnfm";
}
else if (ua.indexOf("Firefox/") > -1) {
url =
"https://addons.mozilla.org/en-US/firefox/addon/apollo-developer-tools/";
}
}
if (url) {
__DEV__ && invariant.log("Download the Apollo DevTools for a better development " +
"experience: %s", url);
}
}
}, 10000);
}
}
}
/**
* The `DocumentTransform` used to modify GraphQL documents before a request
* is made. If a custom `DocumentTransform` is not provided, this will be the
* default document transform.
*/
get documentTransform() {
return this.queryManager.documentTransform;
}
/**
* The configured `LocalState` instance used to enable the use of `@client`
* fields.
*/
get localState() {
return this.queryManager.localState;
}
set localState(localState) {
this.queryManager.localState = localState;
}
/**
* Call this method to terminate any active client processes, making it safe
* to dispose of this `ApolloClient` instance.
*
* This method performs aggressive cleanup to prevent memory leaks:
*
* - Unsubscribes all active `ObservableQuery` instances by emitting a `completed` event
* - Rejects all currently running queries with "QueryManager stopped while query was in flight"
* - Removes all queryRefs from the suspense cache
*/
stop() {
this.queryManager.stop();
}
/**
* This watches the cache store of the query according to the options specified and
* returns an `ObservableQuery`. We can subscribe to this `ObservableQuery` and
* receive updated results through an observer when the cache store changes.
*
* Note that this method is not an implementation of GraphQL subscriptions. Rather,
* it uses Apollo's store in order to reactively deliver updates to your query results.
*
* For example, suppose you call watchQuery on a GraphQL query that fetches a person's
* first and last name and this person has a particular object identifier, provided by
* `cache.identify`. Later, a different query fetches that same person's
* first and last name and the first name has now changed. Then, any observers associated
* with the results of the first query will be updated with a new result object.
*
* Note that if the cache does not change, the subscriber will _not_ be notified.
*
* See [here](https://medium.com/apollo-stack/the-concepts-of-graphql-bc68bd819be3#.3mb0cbcmc) for
* a description of store reactivity.
*/
watchQuery(options) {
if (this.defaultOptions.watchQuery) {
options = mergeOptions(this.defaultOptions.watchQuery, options);
}
return this.queryManager.watchQuery(options);
}
/**
* This resolves a single query according to the options specified and
* returns a `Promise` which is either resolved with the resulting data
* or rejected with an error.
*
* @param options - An object of type `QueryOptions` that allows us to
* describe how this query should be treated e.g. whether it should hit the
* server at all or just resolve from the cache, etc.
*/
query(options) {
if (this.defaultOptions.query) {
options = mergeOptions(this.defaultOptions.query, options);
}
if (__DEV__) {
invariant(options.fetchPolicy !== "cache-and-network", 67);
invariant(options.fetchPolicy !== "standby", 68);
invariant(options.query, 69);
invariant(options.query.kind === "Document", 70);
invariant(!options.returnPartialData, 71);
invariant(!options.pollInterval, 72);
invariant(!options.notifyOnNetworkStatusChange, 73);
}
return this.queryManager.query(options);
}
/**
* This resolves a single mutation according to the options specified and returns a
* Promise which is either resolved with the resulting data or rejected with an
* error. In some cases both `data` and `errors` might be undefined, for example
* when `errorPolicy` is set to `'ignore'`.
*
* It takes options as an object with the following keys and values:
*/
mutate(options) {
const optionsWithDefaults = mergeOptions(compact({
fetchPolicy: "network-only",
errorPolicy: "none",
}, this.defaultOptions.mutate), options);
if (__DEV__) {
invariant(optionsWithDefaults.mutation, 74);
invariant(optionsWithDefaults.fetchPolicy === "network-only" ||
optionsWithDefaults.fetchPolicy === "no-cache", 75);
}
checkDocument(optionsWithDefaults.mutation, OperationTypeNode.MUTATION);
return this.queryManager.mutate(optionsWithDefaults);
}
/**
* This subscribes to a graphql subscription according to the options specified and returns an
* `Observable` which either emits received data or an error.
*/
subscribe(options) {
const cause = {};
const observable = this.queryManager.startGraphQLSubscription(options);
const mapped = observable.pipe(map((result) => ({
...result,
data: this.queryManager.maskOperation({
document: options.query,
data: result.data,
fetchPolicy: options.fetchPolicy,
cause,
}),
})));
return Object.assign(mapped, { restart: observable.restart });
}
readQuery(options, optimistic = false) {
return this.cache.readQuery({ ...options, query: this.transform(options.query) }, optimistic);
}
/**
* Watches the cache store of the fragment according to the options specified
* and returns an `Observable`. We can subscribe to this
* `Observable` and receive updated results through an
* observer when the cache store changes.
*
* You must pass in a GraphQL document with a single fragment or a document
* with multiple fragments that represent what you are reading. If you pass
* in a document with multiple fragments then you must also specify a
* `fragmentName`.
*
* @since 3.10.0
* @param options - An object of type `WatchFragmentOptions` that allows
* the cache to identify the fragment and optionally specify whether to react
* to optimistic updates.
*/
watchFragment(options) {
const dataMasking = this.queryManager.dataMasking;
return this.cache
.watchFragment({
...options,
fragment: this.transform(options.fragment, dataMasking),
})
.pipe(map((result) => {
// The transform will remove fragment spreads from the fragment
// document when dataMasking is enabled. The `maskFragment` function
// remains to apply warnings to fragments marked as
// `@unmask(mode: "migrate")`. Since these warnings are only applied
// in dev, we can skip the masking algorithm entirely for production.
if (__DEV__) {
if (dataMasking) {
const data = this.queryManager.maskFragment({
...options,
data: result.data,
});
return { ...result, data };
}
}
return result;
}));
}
readFragment(options, optimistic = false) {
return this.cache.readFragment({ ...options, fragment: this.transform(options.fragment) }, optimistic);
}
/**
* Writes some data in the shape of the provided GraphQL query directly to
* the store. This method will start at the root query. To start at a
* specific id returned by `cache.identify` then use `writeFragment`.
*/
writeQuery(options) {
const ref = this.cache.writeQuery(options);
if (options.broadcast !== false) {
this.queryManager.broadcastQueries();
}
return ref;
}
/**
* Writes some data in the shape of the provided GraphQL fragment directly to
* the store. This method will write to a GraphQL fragment from any arbitrary
* id that is currently cached, unlike `writeQuery` which will only write
* from the root query.
*
* You must pass in a GraphQL document with a single fragment or a document
* with multiple fragments that represent what you are writing. If you pass
* in a document with multiple fragments then you must also specify a
* `fragmentName`.
*/
writeFragment(options) {
const ref = this.cache.writeFragment(options);
if (options.broadcast !== false) {
this.queryManager.broadcastQueries();
}
return ref;
}
__actionHookForDevTools(cb) {
this.devToolsHookCb = cb;
}
__requestRaw(request) {
return execute(this.link, request, { client: this });
}
/**
* Resets your entire store by clearing out your cache and then re-executing
* all of your active queries. This makes it so that you may guarantee that
* there is no data left in your store from a time before you called this
* method.
*
* `resetStore()` is useful when your user just logged out. You’ve removed the
* user session, and you now want to make sure that any references to data you
* might have fetched while the user session was active is gone.
*
* It is important to remember that `resetStore()` _will_ refetch any active
* queries. This means that any components that might be mounted will execute
* their queries again using your network interface. If you do not want to
* re-execute any queries then you should make sure to stop watching any
* active queries.
*/
resetStore() {
return Promise.resolve()
.then(() => this.queryManager.clearStore({
discardWatches: false,
}))
.then(() => Promise.all(this.resetStoreCallbacks.map((fn) => fn())))
.then(() => this.refetchObservableQueries());
}
/**
* Remove all data from the store. Unlike `resetStore`, `clearStore` will
* not refetch any active queries.
*/
clearStore() {
return Promise.resolve()
.then(() => this.queryManager.clearStore({
discardWatches: true,
}))
.then(() => Promise.all(this.clearStoreCallbacks.map((fn) => fn())));
}
/**
* Allows callbacks to be registered that are executed when the store is
* reset. `onResetStore` returns an unsubscribe function that can be used
* to remove registered callbacks.
*/
onResetStore(cb) {
this.resetStoreCallbacks.push(cb);
return () => {
this.resetStoreCallbacks = this.resetStoreCallbacks.filter((c) => c !== cb);
};
}
/**
* Allows callbacks to be registered that are executed when the store is
* cleared. `onClearStore` returns an unsubscribe function that can be used
* to remove registered callbacks.
*/
onClearStore(cb) {
this.clearStoreCallbacks.push(cb);
return () => {
this.clearStoreCallbacks = this.clearStoreCallbacks.filter((c) => c !== cb);
};
}
/**
* Refetches all of your active queries.
*
* `reFetchObservableQueries()` is useful if you want to bring the client back to proper state in case of a network outage
*
* It is important to remember that `reFetchObservableQueries()` _will_ refetch any active
* queries. This means that any components that might be mounted will execute
* their queries again using your network interface. If you do not want to
* re-execute any queries then you should make sure to stop watching any
* active queries.
* Takes optional parameter `includeStandby` which will include queries in standby-mode when refetching.
*
* Note: `cache-only` queries are not refetched by this function.
*
* @deprecated Please use `refetchObservableQueries` instead.
*/
reFetchObservableQueries;
/**
* Refetches all of your active queries.
*
* `refetchObservableQueries()` is useful if you want to bring the client back to proper state in case of a network outage
*
* It is important to remember that `refetchObservableQueries()` _will_ refetch any active
* queries. This means that any components that might be mounted will execute
* their queries again using your network interface. If you do not want to
* re-execute any queries then you should make sure to stop watching any
* active queries.
* Takes optional parameter `includeStandby` which will include queries in standby-mode when refetching.
*
* Note: `cache-only` queries are not refetched by this function.
*/
refetchObservableQueries(includeStandby) {
return this.queryManager.refetchObservableQueries(includeStandby);
}
/**
* Refetches specified active queries. Similar to "refetchObservableQueries()" but with a specific list of queries.
*
* `refetchQueries()` is useful for use cases to imperatively refresh a selection of queries.
*
* It is important to remember that `refetchQueries()` _will_ refetch specified active
* queries. This means that any components that might be mounted will execute
* their queries again using your network interface. If you do not want to
* re-execute any queries then you should make sure to stop watching any
* active queries.
*/
refetchQueries(options) {
const map = this.queryManager.refetchQueries(options);
const queries = [];
const results = [];
map.forEach((result, obsQuery) => {
queries.push(obsQuery);
results.push(result);
});
const result = Promise.all(results);
// In case you need the raw results immediately, without awaiting
// Promise.all(results):
result.queries = queries;
result.results = results;
// If you decide to ignore the result Promise because you're using
// result.queries and result.results instead, you shouldn't have to worry
// about preventing uncaught rejections for the Promise.all result.
result.catch((error) => {
__DEV__ && invariant.debug(76, error);
});
return result;
}
/**
* Get all currently active `ObservableQuery` objects, in a `Set`.
*
* An "active" query is one that has observers and a `fetchPolicy` other than
* "standby" or "cache-only".
*
* You can include all `ObservableQuery` objects (including the inactive ones)
* by passing "all" instead of "active", or you can include just a subset of
* active queries by passing an array of query names or DocumentNode objects.
*
* Note: This method only returns queries that have active subscribers. Queries
* without subscribers are not tracked by the client.
*/
getObservableQueries(include = "active") {
return this.queryManager.getObservableQueries(include);
}
/**
* Exposes the cache's complete state, in a serializable format for later restoration.
*
* @remarks
*
* This can be useful for debugging in order to inspect the full state of the
* cache.
*
* @param optimistic - Determines whether the result contains data from the
* optimistic layer
*/
extract(optimistic) {
return this.cache.extract(optimistic);
}
/**
* Replaces existing state in the cache (if any) with the values expressed by
* `serializedState`.
*
* Called when hydrating a cache (server side rendering, or offline storage),
* and also (potentially) during hot reloads.
*/
restore(serializedState) {
return this.cache.restore(serializedState);
}
/**
* Define a new ApolloLink (or link chain) that Apollo Client will use.
*/
setLink(newLink) {
this.link = newLink;
}
get defaultContext() {
return this.queryManager.defaultContext;
}
maskedFragmentTransform = new DocumentTransform(removeMaskedFragmentSpreads);
transform(document, dataMasking = false) {
const transformed = this.queryManager.transform(document);
return dataMasking ?
this.maskedFragmentTransform.transformDocument(transformed)
: transformed;
}
}
if (__DEV__) {
ApolloClient.prototype.getMemoryInternals = getApolloClientMemoryInternals;
}
//# sourceMappingURL=ApolloClient.js.map