UNPKG

@apollo/client

Version:

A fully-featured caching GraphQL client.

552 lines (551 loc) 23.1 kB
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